From 9796f19057ae02fc3f805a49c1c83c11ef8868a3 Mon Sep 17 00:00:00 2001
From: nithyatsu <98416062+nithyatsu@users.noreply.github.com>
Date: Wed, 6 Sep 2023 10:05:53 -0700
Subject: [PATCH 01/13] rad init should use current kube context for its
operations (#6212)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
If there was an existing workspace, rad init was using the kube context
from this file, instead of current kube context.
This caused init to fail. We should use current kube context for rad
init and also update workspace to reflect the correct context.
## Type of change
- This pull request fixes a bug in Radius and has an approved issue
(issue link required).
Fixes: #6028
## Auto-generated summary
### ๐ค Generated by Copilot at f048585
### Summary
๐๐งชโป๏ธ
This pull request fixes a test failure and simplifies the code in the
`radinit` package. It updates the expected value for the
`workspace.Connection` field in the `test case for the
`enterInitOptions` function and moves some assignments outside an `if`
statement in the same function.
> _`enterInitOptions`_
> _Fix test, simplify logic_
> _Autumn of refactor_
### Walkthrough
* Simplify workspace name assignment in `enterInitOptions` function
([link](https://github.com/radius-project/radius/pull/6212/files?diff=unified&w=0#diff-a786a8b7ff8510f7a22696b1be6f2a304c325c404b381caff53b758e8e1ec482L115-R122))
* Update test case for `enterInitOptions` function in `options_test.go`
to match the new logic of selecting Kubernetes context
([link](https://github.com/radius-project/radius/pull/6212/files?diff=unified&w=0#diff-8ade53961f66c9f7818a671656cdac5de7967ea3c6dcb1d54a6b63b13e27f653L160-R164))
---
pkg/cli/cmd/radinit/options.go | 18 ++++----
pkg/cli/cmd/radinit/options_test.go | 70 +++++++++++++++++++++++++++--
2 files changed, 76 insertions(+), 12 deletions(-)
diff --git a/pkg/cli/cmd/radinit/options.go b/pkg/cli/cmd/radinit/options.go
index 1b4b586387..350cc8b6a0 100644
--- a/pkg/cli/cmd/radinit/options.go
+++ b/pkg/cli/cmd/radinit/options.go
@@ -111,16 +111,16 @@ func (r *Runner) enterInitOptions(ctx context.Context) (*initOptions, *workspace
options.Recipes.DevRecipes = !r.Full
+ // If the user has a current workspace we should overwrite it.
+ // If the user does not have a current workspace we should create a new one called default and set it as current
+ // If the user does not have a current workspace and has an existing one called default we should overwrite it and set it as current
if ws == nil {
- // Update the workspace with the information we captured about the environment.
- workspace.Name = options.Environment.Name
- workspace.Environment = fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", options.Environment.Name, options.Environment.Name)
- workspace.Scope = fmt.Sprintf("/planes/radius/local/resourceGroups/%s", options.Environment.Name)
- return &options, workspace, nil
+ workspace.Name = "default"
+ } else {
+ workspace.Name = ws.Name
}
+ workspace.Environment = fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", options.Environment.Name, options.Environment.Name)
+ workspace.Scope = fmt.Sprintf("/planes/radius/local/resourceGroups/%s", options.Environment.Name)
+ return &options, workspace, nil
- ws.Environment = fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", options.Environment.Name, options.Environment.Name)
- ws.Scope = fmt.Sprintf("/planes/radius/local/resourceGroups/%s", options.Environment.Name)
-
- return &options, ws, nil
}
diff --git a/pkg/cli/cmd/radinit/options_test.go b/pkg/cli/cmd/radinit/options_test.go
index 12b362796f..8d04393860 100644
--- a/pkg/cli/cmd/radinit/options_test.go
+++ b/pkg/cli/cmd/radinit/options_test.go
@@ -96,7 +96,7 @@ func Test_enterInitOptions(t *testing.T) {
require.NoError(t, err)
expectedWorkspace := workspaces.Workspace{
- Name: "test-env",
+ Name: "default",
Connection: map[string]any{
"context": "kind-kind",
"kind": workspaces.KindKubernetes,
@@ -157,12 +157,76 @@ workspaces:
expectedWorkspace := workspaces.Workspace{
Name: "abc",
Connection: map[string]any{
- "context": "cool-beans",
+ "context": "kind-kind",
+ "kind": workspaces.KindKubernetes,
+ },
+ Environment: "/planes/radius/local/resourceGroups/test-env/providers/Applications.Core/environments/test-env",
+ Scope: "/planes/radius/local/resourceGroups/test-env",
+ }
+ require.Equal(t, expectedWorkspace, *workspace)
+
+ expectedOptions := initOptions{
+ Cluster: clusterOptions{
+ Context: "kind-kind",
+ Install: true,
+ Namespace: "radius-system",
+ Version: version.Version(),
+ },
+ Environment: environmentOptions{
+ Create: true,
+ Name: "test-env",
+ Namespace: "test-namespace",
+ },
+ }
+ require.Equal(t, expectedOptions, *options)
+ })
+
+ t.Run("existing-workspace-with-default-as-an-entry", func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ prompter := prompt.NewMockInterface(ctrl)
+ k8s := kubernetes.NewMockInterface(ctrl)
+ helm := helm.NewMockInterface(ctrl)
+
+ var yaml = `
+workspaces:
+ default: default
+ items:
+ abc:
+ connection:
+ kind: kubernetes
+ context: cool-beans
+ scope: /a/b/c
+ environment: /a/b/c/providers/Applications.Core/environments/ice-cold
+ default:
+ connection:
+ kind: kubernetes
+ context: hot-beans
+ scope: /d/e/f
+ environment: /a/b/c/providers/Applications.Core/environments/hot-coffee
+`
+ v, err := makeConfig(yaml)
+ runner := Runner{Prompter: prompter, KubernetesInterface: k8s, HelmInterface: helm, Full: true, ConfigHolder: &framework.ConfigHolder{Config: v}}
+
+ require.NoError(t, err)
+ initGetKubeContextSuccess(k8s)
+ initKubeContextWithKind(prompter)
+ initHelmMockRadiusNotInstalled(helm)
+ initEnvNamePrompt(prompter, "test-env")
+ initNamespacePrompt(prompter, "test-namespace")
+ initAddCloudProviderPromptNo(prompter)
+ setScaffoldApplicationPromptNo(prompter)
+
+ options, workspace, err := runner.enterInitOptions(context.Background())
+ require.NoError(t, err)
+
+ expectedWorkspace := workspaces.Workspace{
+ Name: "default",
+ Connection: map[string]any{
+ "context": "kind-kind",
"kind": workspaces.KindKubernetes,
},
Environment: "/planes/radius/local/resourceGroups/test-env/providers/Applications.Core/environments/test-env",
Scope: "/planes/radius/local/resourceGroups/test-env",
- Source: "userconfig",
}
require.Equal(t, expectedWorkspace, *workspace)
From 6a9173f962ac9d1fce1c7102ae64b072a517310b Mon Sep 17 00:00:00 2001
From: vinayada1 <28875764+vinayada1@users.noreply.github.com>
Date: Wed, 6 Sep 2023 10:55:41 -0700
Subject: [PATCH 02/13] Detect deployment failures with gateway (#6126)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Detect deployment failures with gateway
## Type of change
- This pull request fixes a bug in Radius and has an approved issue
(issue link required).
Fixes: #6015
## Auto-generated summary
### ๐ค Generated by Copilot at 32c8c4e
### Summary
๐ก๏ธ๐๐
This pull request adds support for the HTTPProxy custom resource from
Contour, which is a way to configure ingress routes for Kubernetes
applications. It does so by using the dynamic client and informer
packages from `k8s.io/client-go` to create, watch, and wait for the
HTTPProxy resources in the Radius RP. It also updates the RBAC rules and
the error handling for the async operation metrics.
> _Oh we are the coders of the Radius RP_
> _We use the dynamic client to talk to the `HTTPProxy`_
> _We watch and we wait for the ingress to be ready_
> _And we heave on the rope on the count of three_
### Walkthrough
* Add support for HTTPProxy resource from Contour, which is a custom
resource that extends the ingress concept
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-13e38bac99402c4ae82609aaba31139f5f9f7c229d1bd955032255cb18f356c6R125-R132),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45L29-R32),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R54-R55),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R84-R88),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-5d6b976ca1401aa4b4ea6fdaf8142700ada0d6ddd9690bb7eed8797f91d33466L89-R89),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R32),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R40-R41),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7L48-R79),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R136-R142),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7L189-R228),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7L204-R241),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R248-R252),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R297-R392),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1R37),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L43-R44),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L111-R112),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L118-R119),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L125-R126),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L132-R133),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L139-R140),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L146-R147),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L153-R154),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L161-R162),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L169-R170),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L176-R177),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L183-R184))
* Use dynamic client and dynamic informer to interact with any
Kubernetes resource, including custom resources, without needing to know
the schema beforehand (`k8s.io/client-go/dynamic` and
`k8s.io/client-go/dynamic/dynamicinformer` packages)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45L29-R32),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R54-R55),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R84-R88),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R40-R41),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1R37),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L43-R44))
* Use HTTPProxy types and constants from Contour
(`github.com/projectcontour/contour/apis/projectcontour/v1` package)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R32))
* Add new RBAC rule to allow Radius RP service account to get, list, and
watch HTTPProxy resources
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-13e38bac99402c4ae82609aaba31139f5f9f7c229d1bd955032255cb18f356c6R125-R132))
* Add new field to `Service` struct to store dynamic client set
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R54-R55))
* Initialize dynamic client set in `NewService` function
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-454529b0ae518dcc56ddbb99b2ff6fe9b343a3f379412d04a7c663c999dd9b45R84-R88))
* Pass dynamic client set from backend service to application model
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-5d6b976ca1401aa4b4ea6fdaf8142700ada0d6ddd9690bb7eed8797f91d33466L89-R89),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L43-R44))
* Pass dynamic client set from application model to Kubernetes handler
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L111-R112),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L118-R119),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L125-R126),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L132-R133),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L139-R140),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L146-R147),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L153-R154),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L161-R162),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L169-R170),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L176-R177),
[link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-e7ee031acac05594d728e23d4e9d0db2ff67d3406372583aeac14a256fcb69e1L183-R184))
* Add new case for "httpproxy" resource type in `Put` method of
Kubernetes handler
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R136-R142))
* Add new method to Kubernetes handler to wait until HTTPProxy resource
is ready or an error occurs (`waitUntilHTTPProxyIsReady`)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R297-R392))
* Add new method to Kubernetes handler to check the status of HTTPProxy
resource and send a nil or an error to a done channel
(`checkHTTPProxyStatus`)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R297-R392))
* Add new method to Kubernetes handler to add an event handler to
HTTPProxy informer (`addHTTPProxyEventHandler`)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7L189-R228))
* Add new function to handlers package to create and get HTTPProxy
informer (`HTTPProxyInformer`)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7R248-R252))
* Add new constants and modify comment for existing constants in
handlers package (`MaxHTTPProxyDeploymentTimeout`,
`ProxyConditionValid`, `MaxDeploymentTimeout`)
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-033dc8a7c9b970cc30f7420ce8cf2f12a26258b276779683ba3f761c481bf0a7L48-R79))
* Improve error handling for async operation metrics by adding nil check
for `res.Error` field before accessing `res.Error.Code` field in
`newAsyncOperationCommonAttributes` function
([link](https://github.com/project-radius/radius/pull/6126/files?diff=unified&w=0#diff-0746ff3ed894513244b3843837993bc5f1de5716bb72e8114c0020cc6821a03bL141-R143))
---
go.mod | 2 +-
pkg/armrpc/asyncoperation/worker/service.go | 10 +
pkg/corerp/backend/service.go | 2 +-
pkg/corerp/handlers/kubernetes.go | 322 +----
.../handlers/kubernetes_deployment_waiter.go | 323 +++++
.../kubernetes_deployment_waiter_test.go | 1116 +++++++++++++++++
.../handlers/kubernetes_http_proxy_waiter.go | 171 +++
.../kubernetes_http_proxy_waiter_test.go | 326 +++++
pkg/corerp/handlers/kubernetes_test.go | 1092 +---------------
pkg/corerp/model/application_model.go | 9 +-
pkg/corerp/renderers/gateway/render.go | 15 +-
pkg/corerp/renderers/gateway/render_test.go | 7 +
.../shared/resources/gateway_test.go | 47 +
.../corerp-resources-gateway-failure.bicep | 54 +
test/functional/shared/rptest.go | 3 +
15 files changed, 2111 insertions(+), 1388 deletions(-)
create mode 100644 pkg/corerp/handlers/kubernetes_deployment_waiter.go
create mode 100644 pkg/corerp/handlers/kubernetes_deployment_waiter_test.go
create mode 100644 pkg/corerp/handlers/kubernetes_http_proxy_waiter.go
create mode 100644 pkg/corerp/handlers/kubernetes_http_proxy_waiter_test.go
create mode 100644 test/functional/shared/resources/testdata/corerp-resources-gateway-failure.bicep
diff --git a/go.mod b/go.mod
index 4e1df70233..213c5e4731 100644
--- a/go.mod
+++ b/go.mod
@@ -67,7 +67,6 @@ require (
github.com/wI2L/jsondiff v0.2.0
go.etcd.io/etcd/client/v3 v3.5.9
go.etcd.io/etcd/server/v3 v3.5.9
- go.mongodb.org/mongo-driver v1.12.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0
go.opentelemetry.io/otel v1.16.0
@@ -98,6 +97,7 @@ require (
github.com/tidwall/gjson v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
+ go.mongodb.org/mongo-driver v1.12.0 // indirect
)
require (
diff --git a/pkg/armrpc/asyncoperation/worker/service.go b/pkg/armrpc/asyncoperation/worker/service.go
index f8fb7c508a..d2ce0cec69 100644
--- a/pkg/armrpc/asyncoperation/worker/service.go
+++ b/pkg/armrpc/asyncoperation/worker/service.go
@@ -29,6 +29,9 @@ import (
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
+
+ "k8s.io/client-go/dynamic"
+
controller_runtime "sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -52,6 +55,8 @@ type Service struct {
KubeClientSet kubernetes.Interface
// KubeDiscoveryClient is the Kubernetes discovery client.
KubeDiscoveryClient discovery.ServerResourcesInterface
+ // KubeDynamicClientSet is the Kubernetes dynamic client.
+ KubeDynamicClientSet dynamic.Interface
}
// Init initializes worker service - it initializes the StorageProvider, RequestQueue, OperationStatusManager, Controllers, KubeClient and
@@ -90,6 +95,11 @@ func (s *Service) Init(ctx context.Context) error {
// TODO: Disable UseLegacyDiscovery once https://github.com/radius-project/radius/issues/5974 is resolved.
discoveryClient.UseLegacyDiscovery = true
s.KubeDiscoveryClient = discoveryClient
+
+ s.KubeDynamicClientSet, err = dynamic.NewForConfig(s.Options.K8sConfig)
+ if err != nil {
+ return err
+ }
}
return nil
}
diff --git a/pkg/corerp/backend/service.go b/pkg/corerp/backend/service.go
index acd679e5b1..931846bcbc 100644
--- a/pkg/corerp/backend/service.go
+++ b/pkg/corerp/backend/service.go
@@ -84,7 +84,7 @@ func (w *Service) Run(ctx context.Context) error {
return err
}
- coreAppModel, err := model.NewApplicationModel(w.Options.Arm, w.KubeClient, w.KubeClientSet, w.KubeDiscoveryClient)
+ coreAppModel, err := model.NewApplicationModel(w.Options.Arm, w.KubeClient, w.KubeClientSet, w.KubeDiscoveryClient, w.KubeDynamicClientSet)
if err != nil {
return fmt.Errorf("failed to initialize application model: %w", err)
}
diff --git a/pkg/corerp/handlers/kubernetes.go b/pkg/corerp/handlers/kubernetes.go
index 11cfb8eeb8..cbcfa770a3 100644
--- a/pkg/corerp/handlers/kubernetes.go
+++ b/pkg/corerp/handlers/kubernetes.go
@@ -31,14 +31,13 @@ import (
resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes"
"github.com/radius-project/radius/pkg/ucp/ucplog"
- v1 "k8s.io/api/apps/v1"
- corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
k8s "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
@@ -46,32 +45,33 @@ import (
)
const (
- // MaxDeploymentTimeout is the max timeout for waiting for a deployment to be ready.
- // Deployment duration should not reach to this timeout since async operation worker will time out context before MaxDeploymentTimeout.
- MaxDeploymentTimeout = time.Minute * time.Duration(10)
// DefaultCacheResyncInterval is the interval for resyncing informer.
DefaultCacheResyncInterval = time.Second * time.Duration(30)
)
+// Create an interface for deployment waiter and http proxy waiter
+type ResourceWaiter interface {
+ addDynamicEventHandler(ctx context.Context, informerFactory dynamicinformer.DynamicSharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error)
+ addEventHandler(ctx context.Context, informerFactory informers.SharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error)
+ waitUntilReady(ctx context.Context, item client.Object) error
+}
+
// NewKubernetesHandler creates a new KubernetesHandler which is used to handle Kubernetes resources.
-func NewKubernetesHandler(client client.Client, clientSet k8s.Interface, discoveryClient discovery.ServerResourcesInterface) ResourceHandler {
+func NewKubernetesHandler(client client.Client, clientSet k8s.Interface, discoveryClient discovery.ServerResourcesInterface, dynamicClientSet dynamic.Interface) ResourceHandler {
return &kubernetesHandler{
- client: client,
- clientSet: clientSet,
- k8sDiscoveryClient: discoveryClient,
- deploymentTimeOut: MaxDeploymentTimeout,
- cacheResyncInterval: DefaultCacheResyncInterval,
+ client: client,
+ k8sDiscoveryClient: discoveryClient,
+ httpProxyWaiter: NewHTTPProxyWaiter(dynamicClientSet),
+ deploymentWaiter: NewDeploymentWaiter(clientSet),
}
}
type kubernetesHandler struct {
- client client.Client
- clientSet k8s.Interface
+ client client.Client
// k8sDiscoveryClient is the Kubernetes client to used for API version lookups on Kubernetes resources. Override this for testing.
k8sDiscoveryClient discovery.ServerResourcesInterface
-
- deploymentTimeOut time.Duration
- cacheResyncInterval time.Duration
+ httpProxyWaiter ResourceWaiter
+ deploymentWaiter ResourceWaiter
}
// Put stores the Kubernetes resource in the cluster and returns the properties of the resource. If the resource is a
@@ -118,297 +118,25 @@ func (handler *kubernetesHandler) Put(ctx context.Context, options *PutOptions)
switch strings.ToLower(item.GetKind()) {
case "deployment":
// Monitor the deployment until it is ready.
- err = handler.waitUntilDeploymentIsReady(ctx, &item)
+ err = handler.deploymentWaiter.waitUntilReady(ctx, &item)
if err != nil {
return nil, err
}
logger.Info(fmt.Sprintf("Deployment %s in namespace %s is ready", item.GetName(), item.GetNamespace()))
return properties, nil
+ case "httpproxy":
+ err = handler.httpProxyWaiter.waitUntilReady(ctx, &item)
+ if err != nil {
+ return nil, err
+ }
+ logger.Info(fmt.Sprintf("HTTP Proxy %s in namespace %s is ready", item.GetName(), item.GetNamespace()))
+ return properties, nil
default:
// We do not monitor the other resource types.
return properties, nil
}
}
-func (handler *kubernetesHandler) waitUntilDeploymentIsReady(ctx context.Context, item client.Object) error {
- logger := ucplog.FromContextOrDiscard(ctx)
-
- // When the deployment is done, an error nil will be sent
- // In case of an error, the error will be sent
- doneCh := make(chan error, 1)
-
- ctx, cancel := context.WithTimeout(ctx, handler.deploymentTimeOut)
- // This ensures that the informer is stopped when this function is returned.
- defer cancel()
-
- err := handler.startInformers(ctx, item, doneCh)
- if err != nil {
- logger.Error(err, "failed to start deployment informer")
- return err
- }
-
- select {
- case <-ctx.Done():
- // Get the final deployment status
- dep, err := handler.clientSet.AppsV1().Deployments(item.GetNamespace()).Get(ctx, item.GetName(), metav1.GetOptions{})
- if err != nil {
- return fmt.Errorf("deployment timed out, name: %s, namespace %s, error occured while fetching latest status: %w", item.GetName(), item.GetNamespace(), err)
- }
-
- // Now get the latest available observation of deployment current state
- // note that there can be a race condition here, by the time it fetches the latest status, deployment might be succeeded
- status := v1.DeploymentCondition{}
- if len(dep.Status.Conditions) > 0 {
- status = dep.Status.Conditions[len(dep.Status.Conditions)-1]
- }
- return fmt.Errorf("deployment timed out, name: %s, namespace %s, status: %s, reason: %s", item.GetName(), item.GetNamespace(), status.Message, status.Reason)
-
- case err := <-doneCh:
- if err == nil {
- logger.Info(fmt.Sprintf("Marking deployment %s in namespace %s as complete", item.GetName(), item.GetNamespace()))
- }
- return err
- }
-}
-
-func (handler *kubernetesHandler) addEventHandler(ctx context.Context, informerFactory informers.SharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error) {
- logger := ucplog.FromContextOrDiscard(ctx)
-
- _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
- AddFunc: func(obj any) {
- handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
- },
- UpdateFunc: func(_, newObj any) {
- handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
- },
- })
-
- if err != nil {
- logger.Error(err, "failed to add event handler")
- }
-}
-
-func (handler *kubernetesHandler) startInformers(ctx context.Context, item client.Object, doneCh chan<- error) error {
- logger := ucplog.FromContextOrDiscard(ctx)
- informerFactory := informers.NewSharedInformerFactoryWithOptions(handler.clientSet, handler.cacheResyncInterval, informers.WithNamespace(item.GetNamespace()))
- // Add event handlers to the pod informer
- handler.addEventHandler(ctx, informerFactory, informerFactory.Core().V1().Pods().Informer(), item, doneCh)
-
- // Add event handlers to the deployment informer
- handler.addEventHandler(ctx, informerFactory, informerFactory.Apps().V1().Deployments().Informer(), item, doneCh)
-
- // Add event handlers to the replicaset informer
- handler.addEventHandler(ctx, informerFactory, informerFactory.Apps().V1().ReplicaSets().Informer(), item, doneCh)
-
- // Start the informers
- informerFactory.Start(ctx.Done())
-
- // Wait for the deployment and pod informer's cache to be synced.
- informerFactory.WaitForCacheSync(ctx.Done())
-
- logger.Info(fmt.Sprintf("Informers started and caches synced for deployment: %s in namespace: %s", item.GetName(), item.GetNamespace()))
- return nil
-}
-
-// Check if all the pods in the deployment are ready
-func (handler *kubernetesHandler) checkDeploymentStatus(ctx context.Context, informerFactory informers.SharedInformerFactory, item client.Object, doneCh chan<- error) bool {
- logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", item.GetName(), "namespace", item.GetNamespace())
-
- // Get the deployment
- deployment, err := informerFactory.Apps().V1().Deployments().Lister().Deployments(item.GetNamespace()).Get(item.GetName())
- if err != nil {
- logger.Info("Unable to find deployment")
- return false
- }
-
- deploymentReplicaSet := handler.getCurrentReplicaSetForDeployment(ctx, informerFactory, deployment)
- if deploymentReplicaSet == nil {
- logger.Info("Unable to find replica set for deployment")
- return false
- }
-
- allReady := handler.checkAllPodsReady(ctx, informerFactory, deployment, deploymentReplicaSet, doneCh)
- if !allReady {
- logger.Info("All pods are not ready yet for deployment")
- return false
- }
-
- // Check if the deployment is ready
- if deployment.Status.ObservedGeneration != deployment.Generation {
- logger.Info(fmt.Sprintf("Deployment status is not ready: Observed generation: %d, Generation: %d, Deployment Replicaset: %s", deployment.Status.ObservedGeneration, deployment.Generation, deploymentReplicaSet.Name))
- return false
- }
-
- // ObservedGeneration should be updated to latest generation to avoid stale replicas
- for _, c := range deployment.Status.Conditions {
- // check for complete deployment condition
- // Reference https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment
- if c.Type == v1.DeploymentProgressing && c.Status == corev1.ConditionTrue && strings.EqualFold(c.Reason, "NewReplicaSetAvailable") {
- logger.Info(fmt.Sprintf("Deployment is ready. Observed generation: %d, Generation: %d, Deployment Replicaset: %s", deployment.Status.ObservedGeneration, deployment.Generation, deploymentReplicaSet.Name))
- doneCh <- nil
- return true
- } else {
- logger.Info(fmt.Sprintf("Deployment status is: %s - %s, Reason: %s, Deployment replicaset: %s", c.Type, c.Status, c.Reason, deploymentReplicaSet.Name))
- }
- }
- return false
-}
-
-// Gets the current replica set for the deployment
-func (handler *kubernetesHandler) getCurrentReplicaSetForDeployment(ctx context.Context, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment) *v1.ReplicaSet {
- if deployment == nil {
- return nil
- }
-
- logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", deployment.Name, "namespace", deployment.Namespace)
-
- // List all replicasets for this deployment
- rl, err := informerFactory.Apps().V1().ReplicaSets().Lister().ReplicaSets(deployment.Namespace).List(labels.Everything())
- if err != nil {
- // This is a valid state which will eventually be resolved. Therefore, only log the error here.
- logger.Info(fmt.Sprintf("Unable to list replicasets for deployment: %s", err.Error()))
- return nil
- }
-
- if len(rl) == 0 {
- // This is a valid state which will eventually be resolved. Therefore, only log the error here.
- return nil
- }
-
- deploymentRevision := deployment.Annotations["deployment.kubernetes.io/revision"]
-
- // Find the latest ReplicaSet associated with the deployment
- for _, rs := range rl {
- if !metav1.IsControlledBy(rs, deployment) {
- continue
- }
- if rs.Annotations == nil {
- continue
- }
- revision, ok := rs.Annotations["deployment.kubernetes.io/revision"]
- if !ok {
- continue
- }
-
- // The first answer here https://stackoverflow.com/questions/59848252/kubectl-retrieving-the-current-new-replicaset-for-a-deployment-in-json-forma
- // looks like the best way to determine the current replicaset.
- // Match the replica set revision with the deployment revision
- if deploymentRevision == revision {
- return rs
- }
- }
-
- return nil
-}
-
-func (handler *kubernetesHandler) checkAllPodsReady(ctx context.Context, informerFactory informers.SharedInformerFactory, obj *v1.Deployment, deploymentReplicaSet *v1.ReplicaSet, doneCh chan<- error) bool {
- logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", obj.GetName(), "namespace", obj.GetNamespace())
- logger.Info("Checking if all pods in the deployment are ready")
-
- podsInDeployment, err := handler.getPodsInDeployment(ctx, informerFactory, obj, deploymentReplicaSet)
- if err != nil {
- logger.Info(fmt.Sprintf("Error getting pods for deployment: %s", err.Error()))
- return false
- }
-
- allReady := true
- for _, pod := range podsInDeployment {
- podReady, err := handler.checkPodStatus(ctx, &pod)
- if err != nil {
- // Terminate the deployment and return the error encountered
- doneCh <- err
- return false
- }
- if !podReady {
- allReady = false
- }
- }
-
- if allReady {
- logger.Info(fmt.Sprintf("All %d pods in the deployment are ready", len(podsInDeployment)))
- }
- return allReady
-}
-
-func (handler *kubernetesHandler) getPodsInDeployment(ctx context.Context, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment, deploymentReplicaSet *v1.ReplicaSet) ([]corev1.Pod, error) {
- logger := ucplog.FromContextOrDiscard(ctx)
-
- pods := []corev1.Pod{}
-
- // List all pods that match the current replica set
- pl, err := informerFactory.Core().V1().Pods().Lister().Pods(deployment.GetNamespace()).List(labels.Set(deployment.Spec.Selector.MatchLabels).AsSelector())
- if err != nil {
- logger.Info(fmt.Sprintf("Unable to find pods for deployment %s in namespace %s", deployment.GetName(), deployment.GetNamespace()))
- return []corev1.Pod{}, nil
- }
-
- // Filter out the pods that are not in the Deployment's current ReplicaSet
- for _, p := range pl {
- if !metav1.IsControlledBy(p, deploymentReplicaSet) {
- continue
- }
- pods = append(pods, *p)
- }
-
- return pods, nil
-}
-
-func (handler *kubernetesHandler) checkPodStatus(ctx context.Context, pod *corev1.Pod) (bool, error) {
- logger := ucplog.FromContextOrDiscard(ctx).WithValues("podName", pod.Name, "namespace", pod.Namespace)
-
- conditionPodReady := true
- for _, cc := range pod.Status.Conditions {
- if cc.Type == corev1.PodReady && cc.Status != corev1.ConditionTrue {
- // Do not return false here else if the pod transitions to a crash loop backoff state,
- // we won't be able to detect that condition.
- conditionPodReady = false
- }
-
- if cc.Type == corev1.ContainersReady && cc.Status != corev1.ConditionTrue {
- // Do not return false here else if the pod transitions to a crash loop backoff state,
- // we won't be able to detect that condition.
- conditionPodReady = false
- }
- }
-
- // Sometimes container statuses are not yet available and we do not want to falsely return that the containers are ready
- if len(pod.Status.ContainerStatuses) <= 0 {
- return false, nil
- }
-
- for _, cs := range pod.Status.ContainerStatuses {
- // Check if the container state is terminated or unable to start due to crash loop, image pull back off or error
- // Note that sometimes a pod can go into running state but can crash later and can go undetected by this condition
- // We will rely on the user defining a readiness probe to ensure that the pod is ready to serve traffic for those cases
- if cs.State.Terminated != nil {
- logger.Info(fmt.Sprintf("Container state is terminated Reason: %s, Message: %s", cs.State.Terminated.Reason, cs.State.Terminated.Message))
- return false, fmt.Errorf("Container state is 'Terminated' Reason: %s, Message: %s", cs.State.Terminated.Reason, cs.State.Terminated.Message)
- } else if cs.State.Waiting != nil {
- if cs.State.Waiting.Reason == "ErrImagePull" || cs.State.Waiting.Reason == "CrashLoopBackOff" || cs.State.Waiting.Reason == "ImagePullBackOff" {
- message := cs.State.Waiting.Message
- if cs.LastTerminationState.Terminated != nil {
- message += " LastTerminationState: " + cs.LastTerminationState.Terminated.Message
- }
- return false, fmt.Errorf("Container state is 'Waiting' Reason: %s, Message: %s", cs.State.Waiting.Reason, message)
- } else {
- return false, nil
- }
- } else if cs.State.Running == nil {
- // The container is not yet running
- return false, nil
- } else if !cs.Ready {
- // The container is running but has not passed its readiness probe yet
- return false, nil
- }
- }
-
- if !conditionPodReady {
- return false, nil
- }
- logger.Info("All containers for pod are ready")
- return true, nil
-}
-
// Delete decodes the identity data from the DeleteOptions, creates an unstructured object from the identity data,
// and then attempts to delete the object from the Kubernetes cluster, returning an error if one occurs.
func (handler *kubernetesHandler) Delete(ctx context.Context, options *DeleteOptions) error {
diff --git a/pkg/corerp/handlers/kubernetes_deployment_waiter.go b/pkg/corerp/handlers/kubernetes_deployment_waiter.go
new file mode 100644
index 0000000000..71d61ebff0
--- /dev/null
+++ b/pkg/corerp/handlers/kubernetes_deployment_waiter.go
@@ -0,0 +1,323 @@
+package handlers
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/radius-project/radius/pkg/ucp/ucplog"
+ v1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/client-go/dynamic/dynamicinformer"
+ "k8s.io/client-go/informers"
+ k8s "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/tools/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ // MaxDeploymentTimeout is the max timeout for waiting for a deployment to be ready.
+ // Deployment duration should not reach to this timeout since async operation worker will time out context before MaxDeploymentTimeout.
+ MaxDeploymentTimeout = time.Minute * time.Duration(10)
+)
+
+type deploymentWaiter struct {
+ clientSet k8s.Interface
+ deploymentTimeOut time.Duration
+ cacheResyncInterval time.Duration
+}
+
+func NewDeploymentWaiter(clientSet k8s.Interface) *deploymentWaiter {
+ return &deploymentWaiter{
+ clientSet: clientSet,
+ deploymentTimeOut: MaxDeploymentTimeout,
+ cacheResyncInterval: DefaultCacheResyncInterval,
+ }
+}
+
+func (handler *deploymentWaiter) addEventHandler(ctx context.Context, informerFactory informers.SharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error) {
+ logger := ucplog.FromContextOrDiscard(ctx)
+
+ _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj any) {
+ handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+ },
+ UpdateFunc: func(_, newObj any) {
+ handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+ },
+ })
+
+ if err != nil {
+ logger.Error(err, "failed to add event handler")
+ }
+}
+
+// addDynamicEventHandler is not implemented for deploymentWaiter
+func (handler *deploymentWaiter) addDynamicEventHandler(ctx context.Context, informerFactory dynamicinformer.DynamicSharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error) {
+}
+
+func (handler *deploymentWaiter) waitUntilReady(ctx context.Context, item client.Object) error {
+ logger := ucplog.FromContextOrDiscard(ctx)
+
+ // When the deployment is done, an error nil will be sent
+ // In case of an error, the error will be sent
+ doneCh := make(chan error, 1)
+
+ ctx, cancel := context.WithTimeout(ctx, handler.deploymentTimeOut)
+ // This ensures that the informer is stopped when this function is returned.
+ defer cancel()
+
+ err := handler.startInformers(ctx, item, doneCh)
+ if err != nil {
+ logger.Error(err, "failed to start deployment informer")
+ return err
+ }
+
+ select {
+ case <-ctx.Done():
+ // Get the final deployment status
+ dep, err := handler.clientSet.AppsV1().Deployments(item.GetNamespace()).Get(ctx, item.GetName(), metav1.GetOptions{})
+ if err != nil {
+ return fmt.Errorf("deployment timed out, name: %s, namespace %s, error occured while fetching latest status: %w", item.GetName(), item.GetNamespace(), err)
+ }
+
+ // Now get the latest available observation of deployment current state
+ // note that there can be a race condition here, by the time it fetches the latest status, deployment might be succeeded
+ status := v1.DeploymentCondition{}
+ if len(dep.Status.Conditions) > 0 {
+ status = dep.Status.Conditions[len(dep.Status.Conditions)-1]
+ }
+ return fmt.Errorf("deployment timed out, name: %s, namespace %s, status: %s, reason: %s", item.GetName(), item.GetNamespace(), status.Message, status.Reason)
+
+ case err := <-doneCh:
+ if err == nil {
+ logger.Info(fmt.Sprintf("Marking deployment %s in namespace %s as complete", item.GetName(), item.GetNamespace()))
+ }
+ return err
+ }
+}
+
+// Check if all the pods in the deployment are ready
+func (handler *deploymentWaiter) checkDeploymentStatus(ctx context.Context, informerFactory informers.SharedInformerFactory, item client.Object, doneCh chan<- error) bool {
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", item.GetName(), "namespace", item.GetNamespace())
+
+ // Get the deployment
+ deployment, err := informerFactory.Apps().V1().Deployments().Lister().Deployments(item.GetNamespace()).Get(item.GetName())
+ if err != nil {
+ logger.Info("Unable to find deployment")
+ return false
+ }
+
+ deploymentReplicaSet := handler.getCurrentReplicaSetForDeployment(ctx, informerFactory, deployment)
+ if deploymentReplicaSet == nil {
+ logger.Info("Unable to find replica set for deployment")
+ return false
+ }
+
+ allReady := handler.checkAllPodsReady(ctx, informerFactory, deployment, deploymentReplicaSet, doneCh)
+ if !allReady {
+ logger.Info("All pods are not ready yet for deployment")
+ return false
+ }
+
+ // Check if the deployment is ready
+ if deployment.Status.ObservedGeneration != deployment.Generation {
+ logger.Info(fmt.Sprintf("Deployment status is not ready: Observed generation: %d, Generation: %d, Deployment Replicaset: %s", deployment.Status.ObservedGeneration, deployment.Generation, deploymentReplicaSet.Name))
+ return false
+ }
+
+ // ObservedGeneration should be updated to latest generation to avoid stale replicas
+ for _, c := range deployment.Status.Conditions {
+ // check for complete deployment condition
+ // Reference https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment
+ if c.Type == v1.DeploymentProgressing && c.Status == corev1.ConditionTrue && strings.EqualFold(c.Reason, "NewReplicaSetAvailable") {
+ logger.Info(fmt.Sprintf("Deployment is ready. Observed generation: %d, Generation: %d, Deployment Replicaset: %s", deployment.Status.ObservedGeneration, deployment.Generation, deploymentReplicaSet.Name))
+ doneCh <- nil
+ return true
+ } else {
+ logger.Info(fmt.Sprintf("Deployment status is: %s - %s, Reason: %s, Deployment replicaset: %s", c.Type, c.Status, c.Reason, deploymentReplicaSet.Name))
+ }
+ }
+ return false
+}
+
+func (handler *deploymentWaiter) startInformers(ctx context.Context, item client.Object, doneCh chan<- error) error {
+ logger := ucplog.FromContextOrDiscard(ctx)
+
+ informerFactory := informers.NewSharedInformerFactoryWithOptions(handler.clientSet, handler.cacheResyncInterval, informers.WithNamespace(item.GetNamespace()))
+ // Add event handlers to the pod informer
+ handler.addEventHandler(ctx, informerFactory, informerFactory.Core().V1().Pods().Informer(), item, doneCh)
+
+ // Add event handlers to the deployment informer
+ handler.addEventHandler(ctx, informerFactory, informerFactory.Apps().V1().Deployments().Informer(), item, doneCh)
+
+ // Add event handlers to the replicaset informer
+ handler.addEventHandler(ctx, informerFactory, informerFactory.Apps().V1().ReplicaSets().Informer(), item, doneCh)
+
+ // Start the informers
+ informerFactory.Start(ctx.Done())
+
+ // Wait for the deployment and pod informer's cache to be synced.
+ informerFactory.WaitForCacheSync(ctx.Done())
+
+ logger.Info(fmt.Sprintf("Informers started and caches synced for deployment: %s in namespace: %s", item.GetName(), item.GetNamespace()))
+ return nil
+}
+
+// Gets the current replica set for the deployment
+func (handler *deploymentWaiter) getCurrentReplicaSetForDeployment(ctx context.Context, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment) *v1.ReplicaSet {
+ if deployment == nil {
+ return nil
+ }
+
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", deployment.Name, "namespace", deployment.Namespace)
+
+ // List all replicasets for this deployment
+ rl, err := informerFactory.Apps().V1().ReplicaSets().Lister().ReplicaSets(deployment.Namespace).List(labels.Everything())
+ if err != nil {
+ // This is a valid state which will eventually be resolved. Therefore, only log the error here.
+ logger.Info(fmt.Sprintf("Unable to list replicasets for deployment: %s", err.Error()))
+ return nil
+ }
+
+ if len(rl) == 0 {
+ // This is a valid state which will eventually be resolved. Therefore, only log the error here.
+ return nil
+ }
+
+ deploymentRevision := deployment.Annotations["deployment.kubernetes.io/revision"]
+
+ // Find the latest ReplicaSet associated with the deployment
+ for _, rs := range rl {
+ if !metav1.IsControlledBy(rs, deployment) {
+ continue
+ }
+ if rs.Annotations == nil {
+ continue
+ }
+ revision, ok := rs.Annotations["deployment.kubernetes.io/revision"]
+ if !ok {
+ continue
+ }
+
+ // The first answer here https://stackoverflow.com/questions/59848252/kubectl-retrieving-the-current-new-replicaset-for-a-deployment-in-json-forma
+ // looks like the best way to determine the current replicaset.
+ // Match the replica set revision with the deployment revision
+ if deploymentRevision == revision {
+ return rs
+ }
+ }
+
+ return nil
+}
+
+func (handler *deploymentWaiter) checkAllPodsReady(ctx context.Context, informerFactory informers.SharedInformerFactory, obj *v1.Deployment, deploymentReplicaSet *v1.ReplicaSet, doneCh chan<- error) bool {
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("deploymentName", obj.GetName(), "namespace", obj.GetNamespace())
+ logger.Info("Checking if all pods in the deployment are ready")
+
+ podsInDeployment, err := handler.getPodsInDeployment(ctx, informerFactory, obj, deploymentReplicaSet)
+ if err != nil {
+ logger.Info(fmt.Sprintf("Error getting pods for deployment: %s", err.Error()))
+ return false
+ }
+
+ allReady := true
+ for _, pod := range podsInDeployment {
+ podReady, err := handler.checkPodStatus(ctx, &pod)
+ if err != nil {
+ // Terminate the deployment and return the error encountered
+ doneCh <- err
+ return false
+ }
+ if !podReady {
+ allReady = false
+ }
+ }
+
+ if allReady {
+ logger.Info(fmt.Sprintf("All %d pods in the deployment are ready", len(podsInDeployment)))
+ }
+ return allReady
+}
+
+func (handler *deploymentWaiter) getPodsInDeployment(ctx context.Context, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment, deploymentReplicaSet *v1.ReplicaSet) ([]corev1.Pod, error) {
+ logger := ucplog.FromContextOrDiscard(ctx)
+
+ pods := []corev1.Pod{}
+
+ // List all pods that match the current replica set
+ pl, err := informerFactory.Core().V1().Pods().Lister().Pods(deployment.GetNamespace()).List(labels.Set(deployment.Spec.Selector.MatchLabels).AsSelector())
+ if err != nil {
+ logger.Info(fmt.Sprintf("Unable to find pods for deployment %s in namespace %s", deployment.GetName(), deployment.GetNamespace()))
+ return []corev1.Pod{}, nil
+ }
+
+ // Filter out the pods that are not in the Deployment's current ReplicaSet
+ for _, p := range pl {
+ if !metav1.IsControlledBy(p, deploymentReplicaSet) {
+ continue
+ }
+ pods = append(pods, *p)
+ }
+
+ return pods, nil
+}
+
+func (handler *deploymentWaiter) checkPodStatus(ctx context.Context, pod *corev1.Pod) (bool, error) {
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("podName", pod.Name, "namespace", pod.Namespace)
+
+ conditionPodReady := true
+ for _, cc := range pod.Status.Conditions {
+ if cc.Type == corev1.PodReady && cc.Status != corev1.ConditionTrue {
+ // Do not return false here else if the pod transitions to a crash loop backoff state,
+ // we won't be able to detect that condition.
+ conditionPodReady = false
+ }
+
+ if cc.Type == corev1.ContainersReady && cc.Status != corev1.ConditionTrue {
+ // Do not return false here else if the pod transitions to a crash loop backoff state,
+ // we won't be able to detect that condition.
+ conditionPodReady = false
+ }
+ }
+
+ // Sometimes container statuses are not yet available and we do not want to falsely return that the containers are ready
+ if len(pod.Status.ContainerStatuses) <= 0 {
+ return false, nil
+ }
+
+ for _, cs := range pod.Status.ContainerStatuses {
+ // Check if the container state is terminated or unable to start due to crash loop, image pull back off or error
+ // Note that sometimes a pod can go into running state but can crash later and can go undetected by this condition
+ // We will rely on the user defining a readiness probe to ensure that the pod is ready to serve traffic for those cases
+ if cs.State.Terminated != nil {
+ logger.Info(fmt.Sprintf("Container state is terminated Reason: %s, Message: %s", cs.State.Terminated.Reason, cs.State.Terminated.Message))
+ return false, fmt.Errorf("Container state is 'Terminated' Reason: %s, Message: %s", cs.State.Terminated.Reason, cs.State.Terminated.Message)
+ } else if cs.State.Waiting != nil {
+ if cs.State.Waiting.Reason == "ErrImagePull" || cs.State.Waiting.Reason == "CrashLoopBackOff" || cs.State.Waiting.Reason == "ImagePullBackOff" {
+ message := cs.State.Waiting.Message
+ if cs.LastTerminationState.Terminated != nil {
+ message += " LastTerminationState: " + cs.LastTerminationState.Terminated.Message
+ }
+ return false, fmt.Errorf("Container state is 'Waiting' Reason: %s, Message: %s", cs.State.Waiting.Reason, message)
+ } else {
+ return false, nil
+ }
+ } else if cs.State.Running == nil {
+ // The container is not yet running
+ return false, nil
+ } else if !cs.Ready {
+ // The container is running but has not passed its readiness probe yet
+ return false, nil
+ }
+ }
+
+ if !conditionPodReady {
+ return false, nil
+ }
+ logger.Info("All containers for pod are ready")
+ return true, nil
+}
diff --git a/pkg/corerp/handlers/kubernetes_deployment_waiter_test.go b/pkg/corerp/handlers/kubernetes_deployment_waiter_test.go
new file mode 100644
index 0000000000..c7b48968a4
--- /dev/null
+++ b/pkg/corerp/handlers/kubernetes_deployment_waiter_test.go
@@ -0,0 +1,1116 @@
+/*
+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 handlers
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/radius-project/radius/pkg/kubernetes"
+ "github.com/radius-project/radius/pkg/to"
+ "github.com/radius-project/radius/test/k8sutil"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ v1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/informers"
+ "k8s.io/client-go/kubernetes/fake"
+)
+
+var testDeployment = &v1.Deployment{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Deployment",
+ APIVersion: "apps/v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ Spec: v1.DeploymentSpec{
+ Replicas: to.Ptr(int32(1)),
+ Selector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "app": "test",
+ },
+ },
+ },
+ Status: v1.DeploymentStatus{
+ Conditions: []v1.DeploymentCondition{
+ {
+ Type: v1.DeploymentProgressing,
+ Status: corev1.ConditionTrue,
+ Reason: "NewReplicaSetAvailable",
+ Message: "Deployment has minimum availability",
+ },
+ },
+ },
+}
+
+func addReplicaSetToDeployment(t *testing.T, ctx context.Context, clientset *fake.Clientset, deployment *v1.Deployment) *v1.ReplicaSet {
+ replicaSet := &v1.ReplicaSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-replicaset-1",
+ Namespace: deployment.Namespace,
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ OwnerReferences: []metav1.OwnerReference{
+ *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
+ Group: v1.SchemeGroupVersion.Group,
+ Version: v1.SchemeGroupVersion.Version,
+ Kind: "Deployment",
+ }),
+ },
+ },
+ }
+
+ // Add the ReplicaSet objects to the fake Kubernetes clientset
+ _, err := clientset.AppsV1().ReplicaSets(replicaSet.Namespace).Create(ctx, replicaSet, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ _, err = clientset.AppsV1().Deployments(deployment.Namespace).Update(ctx, deployment, metav1.UpdateOptions{})
+ require.NoError(t, err)
+
+ return replicaSet
+}
+
+func startInformers(ctx context.Context, clientSet *fake.Clientset, handler *kubernetesHandler) informers.SharedInformerFactory {
+ // Create a fake replicaset informer and start
+ informerFactory := informers.NewSharedInformerFactory(clientSet, 0)
+
+ // Add informers
+ informerFactory.Apps().V1().Deployments().Informer()
+ informerFactory.Apps().V1().ReplicaSets().Informer()
+ informerFactory.Core().V1().Pods().Informer()
+
+ informerFactory.Start(context.Background().Done())
+ informerFactory.WaitForCacheSync(ctx.Done())
+ return informerFactory
+}
+
+func TestWaitUntilReady_NewResource(t *testing.T) {
+ ctx := context.Background()
+
+ // Create first deployment that will be watched
+ deployment := &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ },
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ Spec: v1.DeploymentSpec{
+ Replicas: to.Ptr(int32(1)),
+ Selector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "app": "test",
+ },
+ },
+ },
+ Status: v1.DeploymentStatus{
+ Conditions: []v1.DeploymentCondition{
+ {
+ Type: v1.DeploymentProgressing,
+ Status: corev1.ConditionTrue,
+ Reason: "NewReplicaSetAvailable",
+ Message: "Deployment has minimum availability",
+ },
+ },
+ },
+ }
+
+ clientset := fake.NewSimpleClientset(deployment)
+
+ // The deployment is not marked as ready till we find a replica set. Therefore, we need to create one.
+ addReplicaSetToDeployment(t, ctx, clientset, deployment)
+
+ handler := kubernetesHandler{
+ client: k8sutil.NewFakeKubeClient(nil),
+ deploymentWaiter: &deploymentWaiter{
+ clientSet: clientset,
+ deploymentTimeOut: time.Duration(50) * time.Second,
+ cacheResyncInterval: time.Duration(10) * time.Second,
+ },
+ }
+
+ err := handler.deploymentWaiter.waitUntilReady(ctx, deployment)
+ require.NoError(t, err, "Failed to wait for deployment to be ready")
+}
+
+func TestWaitUntilReady_Timeout(t *testing.T) {
+ ctx := context.Background()
+ // Create first deployment that will be watched
+ deployment := &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ Status: v1.DeploymentStatus{
+ Conditions: []v1.DeploymentCondition{
+ {
+ Type: v1.DeploymentProgressing,
+ Status: corev1.ConditionFalse,
+ Reason: "NewReplicaSetAvailable",
+ Message: "Deployment has minimum availability",
+ },
+ },
+ },
+ }
+
+ deploymentClient := fake.NewSimpleClientset(deployment)
+
+ handler := kubernetesHandler{
+ client: k8sutil.NewFakeKubeClient(nil),
+ deploymentWaiter: &deploymentWaiter{
+ clientSet: deploymentClient,
+ deploymentTimeOut: time.Duration(1) * time.Second,
+ cacheResyncInterval: time.Duration(10) * time.Second,
+ },
+ }
+
+ err := handler.deploymentWaiter.waitUntilReady(ctx, deployment)
+ require.Error(t, err)
+ require.Equal(t, "deployment timed out, name: test-deployment, namespace test-namespace, status: Deployment has minimum availability, reason: NewReplicaSetAvailable", err.Error())
+}
+
+func TestWaitUntilReady_DifferentResourceName(t *testing.T) {
+ ctx := context.Background()
+ // Create first deployment that will be watched
+ deployment := &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ Status: v1.DeploymentStatus{
+ Conditions: []v1.DeploymentCondition{
+ {
+ Type: v1.DeploymentProgressing,
+ Status: corev1.ConditionTrue,
+ Reason: "NewReplicaSetAvailable",
+ Message: "Deployment has minimum availability",
+ },
+ },
+ },
+ }
+
+ clientset := fake.NewSimpleClientset(deployment)
+
+ handler := kubernetesHandler{
+ deploymentWaiter: &deploymentWaiter{
+ clientSet: clientset,
+ deploymentTimeOut: time.Duration(1) * time.Second,
+ cacheResyncInterval: time.Duration(10) * time.Second,
+ },
+ }
+
+ err := handler.deploymentWaiter.waitUntilReady(ctx, &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "not-matched-deployment",
+ Namespace: "test-namespace",
+ },
+ })
+
+ // It must be timed out because the name of the deployment does not match.
+ require.Error(t, err)
+ require.Equal(t, "deployment timed out, name: not-matched-deployment, namespace test-namespace, error occured while fetching latest status: deployments.apps \"not-matched-deployment\" not found", err.Error())
+}
+
+func TestGetPodsInDeployment(t *testing.T) {
+ // Create a fake Kubernetes clientset
+ fakeClient := fake.NewSimpleClientset()
+
+ // Create a Deployment object
+ deployment := &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ Spec: v1.DeploymentSpec{
+ Selector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "app": "test-app",
+ },
+ },
+ },
+ }
+
+ // Create a ReplicaSet object
+ replicaset := &v1.ReplicaSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-replicaset",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test-app",
+ },
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ UID: "1234",
+ },
+ }
+
+ // Create a Pod object
+ pod1 := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test-app",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaset.Name,
+ Controller: to.Ptr(true),
+ UID: "1234",
+ },
+ },
+ },
+ }
+
+ // Create a Pod object
+ pod2 := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod2",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "doesnotmatch",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: "xyz",
+ Controller: to.Ptr(true),
+ UID: "1234",
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err := fakeClient.CoreV1().Pods(pod1.Namespace).Create(context.Background(), pod1, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ _, err = fakeClient.CoreV1().Pods(pod2.Namespace).Create(context.Background(), pod2, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create a KubernetesHandler object with the fake clientset
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+ handler := &kubernetesHandler{
+ deploymentWaiter: deploymentWaiter,
+ }
+
+ ctx := context.Background()
+ informerFactory := startInformers(ctx, fakeClient, handler)
+
+ // Call the getPodsInDeployment function
+ pods, err := deploymentWaiter.getPodsInDeployment(ctx, informerFactory, deployment, replicaset)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(pods))
+ require.Equal(t, pod1.Name, pods[0].Name)
+}
+
+func TestGetCurrentReplicaSetForDeployment(t *testing.T) {
+ // Create a fake Kubernetes clientset
+ fakeClient := fake.NewSimpleClientset()
+
+ // Create a Deployment object
+ deployment := &v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-deployment",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ },
+ }
+
+ // Create a ReplicaSet object with a higher revision than the other ReplicaSet
+ replicaSet1 := &v1.ReplicaSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-replicaset-1",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
+ OwnerReferences: []metav1.OwnerReference{
+ *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
+ Group: v1.SchemeGroupVersion.Group,
+ Version: v1.SchemeGroupVersion.Version,
+ Kind: "Deployment",
+ }),
+ },
+ },
+ }
+ // Create another ReplicaSet object with a lower revision than the other ReplicaSet
+ replicaSet2 := &v1.ReplicaSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-replicaset-2",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "0"},
+ OwnerReferences: []metav1.OwnerReference{
+ *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
+ Group: v1.SchemeGroupVersion.Group,
+ Version: v1.SchemeGroupVersion.Version,
+ Kind: "Deployment",
+ }),
+ },
+ },
+ }
+
+ // Create another ReplicaSet object with a higher revision than the other ReplicaSet
+ replicaSet3 := &v1.ReplicaSet{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-replicaset-3",
+ Namespace: "test-namespace",
+ Annotations: map[string]string{"deployment.kubernetes.io/revision": "3"},
+ OwnerReferences: []metav1.OwnerReference{
+ *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
+ Group: v1.SchemeGroupVersion.Group,
+ Version: v1.SchemeGroupVersion.Version,
+ Kind: "Deployment",
+ }),
+ },
+ },
+ }
+
+ // Add the ReplicaSet objects to the fake Kubernetes clientset
+ _, err := fakeClient.AppsV1().ReplicaSets(replicaSet1.Namespace).Create(context.Background(), replicaSet1, metav1.CreateOptions{})
+ require.NoError(t, err)
+ _, err = fakeClient.AppsV1().ReplicaSets(replicaSet2.Namespace).Create(context.Background(), replicaSet2, metav1.CreateOptions{})
+ require.NoError(t, err)
+ _, err = fakeClient.AppsV1().ReplicaSets(replicaSet2.Namespace).Create(context.Background(), replicaSet3, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ // Add the Deployment object to the fake Kubernetes clientset
+ _, err = fakeClient.AppsV1().Deployments(deployment.Namespace).Create(context.Background(), deployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ // Create a KubernetesHandler object with the fake clientset
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+ handler := &kubernetesHandler{
+ deploymentWaiter: deploymentWaiter,
+ }
+
+ ctx := context.Background()
+ informerFactory := startInformers(ctx, fakeClient, handler)
+
+ // Call the getNewestReplicaSetForDeployment function
+ rs := deploymentWaiter.getCurrentReplicaSetForDeployment(ctx, informerFactory, deployment)
+ require.Equal(t, replicaSet1.Name, rs.Name)
+}
+
+func TestCheckPodStatus(t *testing.T) {
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod",
+ Namespace: "test-namespace",
+ },
+ Status: corev1.PodStatus{},
+ }
+
+ podTests := []struct {
+ podCondition []corev1.PodCondition
+ containerStatus []corev1.ContainerStatus
+ isReady bool
+ expectedError string
+ }{
+ {
+ // Container is in Terminated state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Terminated: &corev1.ContainerStateTerminated{
+ Reason: "Error",
+ Message: "Container terminated due to an error",
+ },
+ },
+ },
+ },
+ isReady: false,
+ expectedError: "Container state is 'Terminated' Reason: Error, Message: Container terminated due to an error",
+ },
+ {
+ // Container is in CrashLoopBackOff state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Waiting: &corev1.ContainerStateWaiting{
+ Reason: "CrashLoopBackOff",
+ Message: "Back-off 5m0s restarting failed container=test-container pod=test-pod",
+ },
+ },
+ },
+ },
+ isReady: false,
+ expectedError: "Container state is 'Waiting' Reason: CrashLoopBackOff, Message: Back-off 5m0s restarting failed container=test-container pod=test-pod",
+ },
+ {
+ // Container is in ErrImagePull state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Waiting: &corev1.ContainerStateWaiting{
+ Reason: "ErrImagePull",
+ Message: "Cannot pull image",
+ },
+ },
+ },
+ },
+ isReady: false,
+ expectedError: "Container state is 'Waiting' Reason: ErrImagePull, Message: Cannot pull image",
+ },
+ {
+ // Container is in ImagePullBackOff state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Waiting: &corev1.ContainerStateWaiting{
+ Reason: "ImagePullBackOff",
+ Message: "ImagePullBackOff",
+ },
+ },
+ },
+ },
+ isReady: false,
+ expectedError: "Container state is 'Waiting' Reason: ImagePullBackOff, Message: ImagePullBackOff",
+ },
+ {
+ // No container statuses available
+ isReady: false,
+ expectedError: "",
+ },
+ {
+ // Container is in Waiting state but not a terminally failed state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Waiting: &corev1.ContainerStateWaiting{
+ Reason: "ContainerCreating",
+ Message: "Container is being created",
+ },
+ },
+ Ready: false,
+ },
+ },
+ isReady: false,
+ expectedError: "",
+ },
+ {
+ // Container's Running state is nil
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Running: nil,
+ },
+ Ready: false,
+ },
+ },
+ isReady: false,
+ expectedError: "",
+ },
+ {
+ // Readiness check is not yet passed
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ Ready: false,
+ },
+ },
+ isReady: false,
+ expectedError: "",
+ },
+ {
+ // Container is in Ready state
+ podCondition: nil,
+ containerStatus: []corev1.ContainerStatus{
+ {
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ Ready: true,
+ },
+ },
+ isReady: true,
+ expectedError: "",
+ },
+ }
+
+ ctx := context.Background()
+ deploymentWaiter := NewDeploymentWaiter(fake.NewSimpleClientset())
+ for _, tc := range podTests {
+ pod.Status.Conditions = tc.podCondition
+ pod.Status.ContainerStatuses = tc.containerStatus
+ isReady, err := deploymentWaiter.checkPodStatus(ctx, pod)
+ if tc.expectedError != "" {
+ require.Error(t, err)
+ require.Equal(t, tc.expectedError, err.Error())
+ } else {
+ require.NoError(t, err)
+ }
+ require.Equal(t, tc.isReady, isReady)
+ }
+}
+
+func TestCheckAllPodsReady_Success(t *testing.T) {
+ // Create a fake Kubernetes clientset
+ clientset := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+
+ _, err := clientset.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ replicaSet := addReplicaSetToDeployment(t, ctx, clientset, testDeployment)
+
+ // Create a pod
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ },
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test-container",
+ Image: "test-image",
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Phase: corev1.PodRunning,
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ },
+ },
+ },
+ }
+ _, err = clientset.CoreV1().Pods("test-namespace").Create(context.Background(), pod, metav1.CreateOptions{})
+ assert.NoError(t, err)
+
+ // Create an informer factory and add the deployment and replica set to the cache
+ informerFactory := informers.NewSharedInformerFactory(clientset, 0)
+ addTestObjects(t, clientset, informerFactory, testDeployment, replicaSet, pod)
+
+ // Create a done channel
+ doneCh := make(chan error)
+
+ // Create a handler with the fake clientset
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: clientset,
+ }
+
+ // Call the checkAllPodsReady function
+ allReady := deploymentWaiter.checkAllPodsReady(ctx, informerFactory, testDeployment, replicaSet, doneCh)
+
+ // Check that all pods are ready
+ require.True(t, allReady)
+}
+
+func TestCheckAllPodsReady_Fail(t *testing.T) {
+ // Create a fake Kubernetes clientset
+ clientset := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+
+ _, err := clientset.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ replicaSet := addReplicaSetToDeployment(t, ctx, clientset, testDeployment)
+
+ // Create a pod
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaSet.Name,
+ Controller: to.Ptr(true),
+ },
+ },
+ },
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{
+ {
+ Name: "test-container",
+ Image: "test-image",
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Phase: corev1.PodRunning,
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: false,
+ },
+ },
+ },
+ }
+ _, err = clientset.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ // Create an informer factory and add the deployment and replica set to the cache
+ informerFactory := informers.NewSharedInformerFactory(clientset, 0)
+ addTestObjects(t, clientset, informerFactory, testDeployment, replicaSet, pod)
+
+ // Create a done channel
+ doneCh := make(chan error)
+
+ // Create a handler with the fake clientset
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: clientset,
+ }
+
+ // Call the checkAllPodsReady function
+ allReady := deploymentWaiter.checkAllPodsReady(ctx, informerFactory, testDeployment, replicaSet, doneCh)
+
+ // Check that all pods are ready
+ require.False(t, allReady)
+}
+
+func TestCheckDeploymentStatus_AllReady(t *testing.T) {
+ // Create a fake Kubernetes fakeClient
+ fakeClient := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+ _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+ replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
+
+ // Create a Pod object
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaSet.Name,
+ Controller: to.Ptr(true),
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Conditions: []corev1.PodCondition{
+ {
+ Type: corev1.PodScheduled,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create an informer factory and add the deployment to the cache
+ informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
+ addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
+
+ // Create a fake item and object
+ item := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ },
+ },
+ }
+
+ // Create a done channel
+ doneCh := make(chan error, 1)
+
+ // Call the checkDeploymentStatus function
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+
+ deploymentWaiter.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+
+ err = <-doneCh
+
+ // Check that the deployment readiness was checked
+ require.Nil(t, err)
+}
+
+func TestCheckDeploymentStatus_NoReplicaSetsFound(t *testing.T) {
+ // Create a fake Kubernetes fakeClient
+ fakeClient := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+ _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+
+ // Create a Pod object
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ },
+ Status: corev1.PodStatus{
+ Conditions: []corev1.PodCondition{
+ {
+ Type: corev1.PodScheduled,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create an informer factory and add the deployment to the cache
+ informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
+ err = informerFactory.Apps().V1().Deployments().Informer().GetIndexer().Add(testDeployment)
+ require.NoError(t, err, "Failed to add deployment to informer cache")
+ err = informerFactory.Core().V1().Pods().Informer().GetIndexer().Add(pod)
+ require.NoError(t, err, "Failed to add pod to informer cache")
+ // Note: No replica set added
+
+ // Create a fake item and object
+ item := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ },
+ },
+ }
+
+ // Create a done channel
+ doneCh := make(chan error, 1)
+
+ // Call the checkDeploymentStatus function
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+
+ allReady := deploymentWaiter.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+
+ // Check that the deployment readiness was checked
+ require.False(t, allReady)
+}
+
+func TestCheckDeploymentStatus_PodsNotReady(t *testing.T) {
+ // Create a fake Kubernetes fakeClient
+ fakeClient := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+ _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+ replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
+
+ // Create a Pod object
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaSet.Name,
+ Controller: to.Ptr(true),
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Conditions: []corev1.PodCondition{
+ {
+ Type: corev1.PodScheduled,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ State: corev1.ContainerState{
+ Terminated: &corev1.ContainerStateTerminated{
+ Reason: "Error",
+ Message: "Container terminated due to an error",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create an informer factory and add the deployment to the cache
+ informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
+ addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
+
+ // Create a fake item and object
+ item := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ },
+ },
+ }
+
+ // Create a done channel
+ doneCh := make(chan error, 1)
+
+ // Call the checkDeploymentStatus function
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+
+ go deploymentWaiter.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+ err = <-doneCh
+
+ // Check that the deployment readiness was checked
+ require.Error(t, err)
+ require.Equal(t, err.Error(), "Container state is 'Terminated' Reason: Error, Message: Container terminated due to an error")
+}
+
+func TestCheckDeploymentStatus_ObservedGenerationMismatch(t *testing.T) {
+ // Modify testDeployment to have a different generation than the observed generation
+ generationMismatchDeployment := testDeployment.DeepCopy()
+ generationMismatchDeployment.Generation = 2
+
+ // Create a fake Kubernetes fakeClient
+ fakeClient := fake.NewSimpleClientset()
+
+ ctx := context.Background()
+ _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, generationMismatchDeployment, metav1.CreateOptions{})
+ require.NoError(t, err)
+ replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, generationMismatchDeployment)
+
+ // Create a Pod object
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaSet.Name,
+ Controller: to.Ptr(true),
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Conditions: []corev1.PodCondition{
+ {
+ Type: corev1.PodScheduled,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create an informer factory and add the deployment to the cache
+ informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
+ addTestObjects(t, fakeClient, informerFactory, generationMismatchDeployment, replicaSet, pod)
+
+ // Create a fake item and object
+ item := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ },
+ },
+ }
+
+ // Create a done channel
+ doneCh := make(chan error, 1)
+
+ // Call the checkDeploymentStatus function
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+
+ deploymentWaiter.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+
+ // Check that the deployment readiness was checked
+ require.Zero(t, len(doneCh))
+}
+
+func TestCheckDeploymentStatus_DeploymentNotProgressing(t *testing.T) {
+ // Create a fake Kubernetes fakeClient
+ fakeClient := fake.NewSimpleClientset()
+
+ deploymentNotProgressing := testDeployment.DeepCopy()
+
+ ctx := context.Background()
+ _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, deploymentNotProgressing, metav1.CreateOptions{})
+ require.NoError(t, err)
+ replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, deploymentNotProgressing)
+
+ // Create a Pod object
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "test",
+ },
+ OwnerReferences: []metav1.OwnerReference{
+ {
+ Kind: "ReplicaSet",
+ Name: replicaSet.Name,
+ Controller: to.Ptr(true),
+ },
+ },
+ },
+ Status: corev1.PodStatus{
+ Conditions: []corev1.PodCondition{
+ {
+ Type: corev1.PodScheduled,
+ Status: corev1.ConditionTrue,
+ },
+ },
+ ContainerStatuses: []corev1.ContainerStatus{
+ {
+ Name: "test-container",
+ Ready: true,
+ State: corev1.ContainerState{
+ Running: &corev1.ContainerStateRunning{},
+ },
+ },
+ },
+ },
+ }
+
+ // Add the Pod object to the fake Kubernetes clientset
+ _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
+ require.NoError(t, err, "Failed to create Pod: %v", err)
+
+ // Create an informer factory and add the deployment to the cache
+ informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
+ addTestObjects(t, fakeClient, informerFactory, deploymentNotProgressing, replicaSet, pod)
+
+ deploymentNotProgressing.Status = v1.DeploymentStatus{
+ Conditions: []v1.DeploymentCondition{
+ {
+ Type: v1.DeploymentProgressing,
+ Status: corev1.ConditionFalse,
+ Reason: "NewReplicaSetAvailable",
+ Message: "Deployment has minimum availability",
+ },
+ },
+ }
+
+ // Create a fake item and object
+ item := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ },
+ },
+ }
+
+ // Create a done channel
+ doneCh := make(chan error, 1)
+
+ // Call the checkDeploymentStatus function
+ deploymentWaiter := &deploymentWaiter{
+ clientSet: fakeClient,
+ }
+
+ ready := deploymentWaiter.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
+ require.False(t, ready)
+}
+
+func addTestObjects(t *testing.T, fakeClient *fake.Clientset, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment, replicaSet *v1.ReplicaSet, pod *corev1.Pod) {
+ err := informerFactory.Apps().V1().Deployments().Informer().GetIndexer().Add(deployment)
+ require.NoError(t, err, "Failed to add deployment to informer cache")
+ err = informerFactory.Apps().V1().ReplicaSets().Informer().GetIndexer().Add(replicaSet)
+ require.NoError(t, err, "Failed to add replica set to informer cache")
+ err = informerFactory.Core().V1().Pods().Informer().GetIndexer().Add(pod)
+ require.NoError(t, err, "Failed to add pod to informer cache")
+}
diff --git a/pkg/corerp/handlers/kubernetes_http_proxy_waiter.go b/pkg/corerp/handlers/kubernetes_http_proxy_waiter.go
new file mode 100644
index 0000000000..03edcfd88b
--- /dev/null
+++ b/pkg/corerp/handlers/kubernetes_http_proxy_waiter.go
@@ -0,0 +1,171 @@
+package handlers
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1"
+ "github.com/radius-project/radius/pkg/kubernetes"
+ "github.com/radius-project/radius/pkg/ucp/ucplog"
+
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/dynamic/dynamicinformer"
+ "k8s.io/client-go/informers"
+ "k8s.io/client-go/tools/cache"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ MaxHTTPProxyDeploymentTimeout = time.Minute * time.Duration(10)
+ HTTPProxyConditionValid = "Valid"
+ HTTPProxyStatusInvalid = "invalid"
+ HTTPProxyStatusValid = "valid"
+)
+
+type httpProxyWaiter struct {
+ dynamicClientSet dynamic.Interface
+ httpProxyDeploymentTimeout time.Duration
+ cacheResyncInterval time.Duration
+}
+
+// NewHTTPProxyWaiter returns a new instance of HTTPProxyWaiter
+func NewHTTPProxyWaiter(dynamicClientSet dynamic.Interface) *httpProxyWaiter {
+ return &httpProxyWaiter{
+ dynamicClientSet: dynamicClientSet,
+ httpProxyDeploymentTimeout: MaxHTTPProxyDeploymentTimeout,
+ cacheResyncInterval: DefaultCacheResyncInterval,
+ }
+}
+
+func (handler *httpProxyWaiter) addDynamicEventHandler(ctx context.Context, informerFactory dynamicinformer.DynamicSharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error) {
+ logger := ucplog.FromContextOrDiscard(ctx)
+
+ _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj any) {
+ handler.checkHTTPProxyStatus(ctx, informerFactory, item, doneCh)
+ },
+ UpdateFunc: func(_, newObj any) {
+ handler.checkHTTPProxyStatus(ctx, informerFactory, item, doneCh)
+ },
+ })
+
+ if err != nil {
+ logger.Error(err, "failed to add event handler")
+ }
+}
+
+// addEventHandler is not implemented for HTTPProxyWaiter
+func (handler *httpProxyWaiter) addEventHandler(ctx context.Context, informerFactory informers.SharedInformerFactory, informer cache.SharedIndexInformer, item client.Object, doneCh chan<- error) {
+}
+
+func (handler *httpProxyWaiter) waitUntilReady(ctx context.Context, obj client.Object) error {
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("httpProxyName", obj.GetName(), "namespace", obj.GetNamespace())
+
+ doneCh := make(chan error, 1)
+
+ ctx, cancel := context.WithTimeout(ctx, handler.httpProxyDeploymentTimeout)
+ // This ensures that the informer is stopped when this function is returned.
+ defer cancel()
+
+ // Create dynamic informer for HTTPProxy
+ dynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(handler.dynamicClientSet, 0, obj.GetNamespace(), nil)
+ httpProxyInformer := dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR)
+ // Add event handlers to the http proxy informer
+ handler.addDynamicEventHandler(ctx, dynamicInformerFactory, httpProxyInformer.Informer(), obj, doneCh)
+
+ // Start the informers
+ dynamicInformerFactory.Start(ctx.Done())
+
+ // Wait for the cache to be synced.
+ dynamicInformerFactory.WaitForCacheSync(ctx.Done())
+
+ select {
+ case <-ctx.Done():
+ // Get the final status
+ proxy, err := httpProxyInformer.Lister().Get(obj.GetName())
+
+ if err != nil {
+ return fmt.Errorf("proxy deployment timed out, name: %s, namespace %s, error occured while fetching latest status: %w", obj.GetName(), obj.GetNamespace(), err)
+ }
+
+ p := contourv1.HTTPProxy{}
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(proxy.(*unstructured.Unstructured).Object, &p)
+ if err != nil {
+ return fmt.Errorf("proxy deployment timed out, name: %s, namespace %s, error occured while fetching latest status: %w", obj.GetName(), obj.GetNamespace(), err)
+ }
+
+ status := contourv1.DetailedCondition{}
+ if len(p.Status.Conditions) > 0 {
+ status = p.Status.Conditions[len(p.Status.Conditions)-1]
+ }
+ return fmt.Errorf("HTTP proxy deployment timed out, name: %s, namespace %s, status: %s, reason: %s", obj.GetName(), obj.GetNamespace(), status.Message, status.Reason)
+ case err := <-doneCh:
+ if err == nil {
+ logger.Info(fmt.Sprintf("Marking HTTP proxy deployment %s in namespace %s as complete", obj.GetName(), obj.GetNamespace()))
+ }
+ return err
+ }
+}
+
+func (handler *httpProxyWaiter) checkHTTPProxyStatus(ctx context.Context, dynamicInformerFactory dynamicinformer.DynamicSharedInformerFactory, obj client.Object, doneCh chan<- error) bool {
+ logger := ucplog.FromContextOrDiscard(ctx).WithValues("httpProxyName", obj.GetName(), "namespace", obj.GetNamespace())
+ selector := labels.SelectorFromSet(
+ map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ kubernetes.LabelName: obj.GetName(),
+ },
+ )
+ proxies, err := dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR).Lister().List(selector)
+ if err != nil {
+ logger.Info(fmt.Sprintf("Unable to list http proxies: %s", err.Error()))
+ return false
+ }
+
+ for _, proxy := range proxies {
+ p := contourv1.HTTPProxy{}
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(proxy.(*unstructured.Unstructured).Object, &p)
+ if err != nil {
+ logger.Info(fmt.Sprintf("Unable to convert http proxy: %s", err.Error()))
+ continue
+ }
+
+ if len(p.Spec.Includes) == 0 && len(p.Spec.Routes) > 0 {
+ // This is a route HTTP proxy. We will not validate deployment completion for it and return success here
+ logger.Info("Not validating the deployment of route HTTP proxy for ", p.Name)
+ doneCh <- nil
+ return true
+ }
+
+ // We will check the status for the root HTTP proxy
+ if p.Status.CurrentStatus == HTTPProxyStatusInvalid {
+ if strings.Contains(p.Status.Description, "see Errors for details") {
+ var msg string
+ for _, c := range p.Status.Conditions {
+ if c.ObservedGeneration != p.Generation {
+ continue
+ }
+ if c.Type == HTTPProxyConditionValid && c.Status == "False" {
+ for _, e := range c.Errors {
+ msg += fmt.Sprintf("Error - Type: %s, Status: %s, Reason: %s, Message: %s\n", e.Type, e.Status, e.Reason, e.Message)
+ }
+ }
+ }
+ doneCh <- errors.New(msg)
+ } else {
+ doneCh <- fmt.Errorf("Failed to deploy HTTP proxy. Description: %s", p.Status.Description)
+ }
+ return false
+ } else if p.Status.CurrentStatus == HTTPProxyStatusValid {
+ // The HTTPProxy is ready
+ doneCh <- nil
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/corerp/handlers/kubernetes_http_proxy_waiter_test.go b/pkg/corerp/handlers/kubernetes_http_proxy_waiter_test.go
new file mode 100644
index 0000000000..44837ef551
--- /dev/null
+++ b/pkg/corerp/handlers/kubernetes_http_proxy_waiter_test.go
@@ -0,0 +1,326 @@
+/*
+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 handlers
+
+import (
+ "context"
+ "testing"
+
+ contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1"
+ "github.com/radius-project/radius/pkg/kubernetes"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/dynamic/dynamicinformer"
+ fakedynamic "k8s.io/client-go/dynamic/fake"
+)
+
+func TestCheckHTTPProxyStatus_ValidStatus(t *testing.T) {
+
+ httpProxy := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ Labels: map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ kubernetes.LabelName: "example.com",
+ },
+ },
+ Status: contourv1.HTTPProxyStatus{
+ CurrentStatus: HTTPProxyStatusValid,
+ },
+ }
+ // create fake dynamic clientset
+ s := runtime.NewScheme()
+ err := contourv1.AddToScheme(s)
+ require.NoError(t, err)
+ fakeClient := fakedynamic.NewSimpleDynamicClient(s, httpProxy)
+
+ // create a fake dynamic informer factory with a mock HTTPProxy informer
+ dynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, "default", nil)
+ err = dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR).Informer().GetIndexer().Add(httpProxy)
+ require.NoError(t, err, "Could not add test http proxy to informer cache")
+
+ // create a mock object
+ obj := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ },
+ }
+
+ // create a channel for the done signal
+ doneCh := make(chan error)
+
+ httpProxyWaiter := &httpProxyWaiter{
+ dynamicClientSet: fakeClient,
+ }
+
+ ctx := context.Background()
+ dynamicInformerFactory.Start(ctx.Done())
+ dynamicInformerFactory.WaitForCacheSync(ctx.Done())
+
+ // call the function with the fake clientset, informer factory, logger, object, and done channel
+ go httpProxyWaiter.checkHTTPProxyStatus(context.Background(), dynamicInformerFactory, obj, doneCh)
+ err = <-doneCh
+ require.NoError(t, err)
+}
+
+func TestCheckHTTPProxyStatus_InvalidStatusForRootProxy(t *testing.T) {
+
+ httpProxy := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ Labels: map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ kubernetes.LabelName: "example.com",
+ },
+ },
+ Spec: contourv1.HTTPProxySpec{
+ VirtualHost: &contourv1.VirtualHost{
+ Fqdn: "example.com",
+ },
+ Includes: []contourv1.Include{
+ {
+ Name: "example.com",
+ Namespace: "default",
+ },
+ },
+ },
+ Status: contourv1.HTTPProxyStatus{
+ CurrentStatus: HTTPProxyStatusInvalid,
+ Description: "Failed to deploy HTTP proxy. see Errors for details",
+ Conditions: []contourv1.DetailedCondition{
+ {
+ // specify Condition of type json
+ Condition: metav1.Condition{
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ },
+ Errors: []contourv1.SubCondition{
+ {
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ Reason: "RouteNotDefined",
+ Message: "HTTPProxy is invalid",
+ },
+ },
+ },
+ },
+ },
+ }
+ // create fake dynamic clientset
+ s := runtime.NewScheme()
+ err := contourv1.AddToScheme(s)
+ require.NoError(t, err)
+ fakeClient := fakedynamic.NewSimpleDynamicClient(s, httpProxy)
+
+ // create a fake dynamic informer factory with a mock HTTPProxy informer
+ dynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, "default", nil)
+ err = dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR).Informer().GetIndexer().Add(httpProxy)
+ require.NoError(t, err, "Could not add test http proxy to informer cache")
+
+ // create a mock object
+ obj := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ },
+ }
+
+ // create a channel for the done signal
+ doneCh := make(chan error)
+
+ httpProxyWaiter := &httpProxyWaiter{
+ dynamicClientSet: fakeClient,
+ }
+
+ ctx := context.Background()
+ dynamicInformerFactory.Start(ctx.Done())
+ dynamicInformerFactory.WaitForCacheSync(ctx.Done())
+
+ // call the function with the fake clientset, informer factory, logger, object, and done channel
+ go httpProxyWaiter.checkHTTPProxyStatus(context.Background(), dynamicInformerFactory, obj, doneCh)
+ err = <-doneCh
+ require.EqualError(t, err, "Error - Type: Valid, Status: False, Reason: RouteNotDefined, Message: HTTPProxy is invalid\n")
+}
+
+func TestCheckHTTPProxyStatus_InvalidStatusForRouteProxy(t *testing.T) {
+ httpProxyRoute := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ Labels: map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ kubernetes.LabelName: "example.com",
+ },
+ },
+ Spec: contourv1.HTTPProxySpec{
+ Routes: []contourv1.Route{
+ {
+ Conditions: []contourv1.MatchCondition{
+ {
+ Prefix: "/",
+ },
+ },
+ Services: []contourv1.Service{
+ {
+ Name: "test",
+ Port: 80,
+ },
+ },
+ },
+ },
+ },
+ Status: contourv1.HTTPProxyStatus{
+ CurrentStatus: HTTPProxyStatusInvalid,
+ Description: "Failed to deploy HTTP proxy. see Errors for details",
+ Conditions: []contourv1.DetailedCondition{
+ {
+ // specify Condition of type json
+ Condition: metav1.Condition{
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ },
+ Errors: []contourv1.SubCondition{
+ {
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ Reason: "orphaned",
+ Message: "HTTPProxy is invalid",
+ },
+ },
+ },
+ },
+ },
+ }
+ // create fake dynamic clientset
+ s := runtime.NewScheme()
+ err := contourv1.AddToScheme(s)
+ require.NoError(t, err)
+ fakeClient := fakedynamic.NewSimpleDynamicClient(s, httpProxyRoute)
+
+ // create a fake dynamic informer factory with a mock HTTPProxy informer
+ dynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, "default", nil)
+ err = dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR).Informer().GetIndexer().Add(httpProxyRoute)
+ require.NoError(t, err, "Could not add test http proxy to informer cache")
+
+ // create a mock object
+ obj := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ },
+ }
+
+ // create a channel for the done signal
+ doneCh := make(chan error)
+
+ httpProxyWaiter := &httpProxyWaiter{
+ dynamicClientSet: fakeClient,
+ }
+
+ ctx := context.Background()
+ dynamicInformerFactory.Start(ctx.Done())
+ dynamicInformerFactory.WaitForCacheSync(ctx.Done())
+
+ // call the function with the fake clientset, informer factory, logger, object, and done channel
+ go httpProxyWaiter.checkHTTPProxyStatus(context.Background(), dynamicInformerFactory, obj, doneCh)
+ err = <-doneCh
+ require.NoError(t, err)
+}
+
+func TestCheckHTTPProxyStatus_WrongSelector(t *testing.T) {
+
+ httpProxy := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "abcd.com",
+ Labels: map[string]string{
+ kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
+ kubernetes.LabelName: "abcd.com",
+ },
+ },
+ Spec: contourv1.HTTPProxySpec{
+ VirtualHost: &contourv1.VirtualHost{
+ Fqdn: "abcd.com",
+ },
+ Includes: []contourv1.Include{
+ {
+ Name: "abcd.com",
+ Namespace: "default",
+ },
+ },
+ },
+ Status: contourv1.HTTPProxyStatus{
+ CurrentStatus: HTTPProxyStatusInvalid,
+ Description: "Failed to deploy HTTP proxy. see Errors for details",
+ Conditions: []contourv1.DetailedCondition{
+ {
+ // specify Condition of type json
+ Condition: metav1.Condition{
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ },
+ Errors: []contourv1.SubCondition{
+ {
+ Type: HTTPProxyConditionValid,
+ Status: contourv1.ConditionFalse,
+ Reason: "RouteNotDefined",
+ Message: "HTTPProxy is invalid",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ // create fake dynamic clientset
+ s := runtime.NewScheme()
+ err := contourv1.AddToScheme(s)
+ require.NoError(t, err)
+ fakeClient := fakedynamic.NewSimpleDynamicClient(s, httpProxy)
+
+ // create a fake dynamic informer factory with a mock HTTPProxy informer
+ dynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(fakeClient, 0, "default", nil)
+ err = dynamicInformerFactory.ForResource(contourv1.HTTPProxyGVR).Informer().GetIndexer().Add(httpProxy)
+ require.NoError(t, err, "Could not add test http proxy to informer cache")
+
+ // create a mock object
+ obj := &contourv1.HTTPProxy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: "default",
+ Name: "example.com",
+ },
+ }
+
+ // create a channel for the done signal
+ doneCh := make(chan error)
+
+ httpProxyWaiter := &httpProxyWaiter{
+ dynamicClientSet: fakeClient,
+ }
+
+ ctx := context.Background()
+ dynamicInformerFactory.Start(ctx.Done())
+ dynamicInformerFactory.WaitForCacheSync(ctx.Done())
+
+ // call the function with the fake clientset, informer factory, logger, object, and done channel
+ status := httpProxyWaiter.checkHTTPProxyStatus(context.Background(), dynamicInformerFactory, obj, doneCh)
+ require.False(t, status)
+}
diff --git a/pkg/corerp/handlers/kubernetes_test.go b/pkg/corerp/handlers/kubernetes_test.go
index 11d1c87e13..0403b340f9 100644
--- a/pkg/corerp/handlers/kubernetes_test.go
+++ b/pkg/corerp/handlers/kubernetes_test.go
@@ -22,94 +22,19 @@ import (
"testing"
"time"
- "github.com/radius-project/radius/pkg/kubernetes"
"github.com/radius-project/radius/pkg/resourcemodel"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
- "github.com/radius-project/radius/pkg/to"
resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes"
"github.com/radius-project/radius/test/k8sutil"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
)
-var testDeployment = &v1.Deployment{
- TypeMeta: metav1.TypeMeta{
- Kind: "Deployment",
- APIVersion: "apps/v1",
- },
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- Spec: v1.DeploymentSpec{
- Replicas: to.Ptr(int32(1)),
- Selector: &metav1.LabelSelector{
- MatchLabels: map[string]string{
- "app": "test",
- },
- },
- },
- Status: v1.DeploymentStatus{
- Conditions: []v1.DeploymentCondition{
- {
- Type: v1.DeploymentProgressing,
- Status: corev1.ConditionTrue,
- Reason: "NewReplicaSetAvailable",
- Message: "Deployment has minimum availability",
- },
- },
- },
-}
-
-func addReplicaSetToDeployment(t *testing.T, ctx context.Context, clientset *fake.Clientset, deployment *v1.Deployment) *v1.ReplicaSet {
- replicaSet := &v1.ReplicaSet{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-replicaset-1",
- Namespace: deployment.Namespace,
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- OwnerReferences: []metav1.OwnerReference{
- *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
- Group: v1.SchemeGroupVersion.Group,
- Version: v1.SchemeGroupVersion.Version,
- Kind: "Deployment",
- }),
- },
- },
- }
-
- // Add the ReplicaSet objects to the fake Kubernetes clientset
- _, err := clientset.AppsV1().ReplicaSets(replicaSet.Namespace).Create(ctx, replicaSet, metav1.CreateOptions{})
- require.NoError(t, err)
-
- _, err = clientset.AppsV1().Deployments(deployment.Namespace).Update(ctx, deployment, metav1.UpdateOptions{})
- require.NoError(t, err)
-
- return replicaSet
-}
-
-func startInformers(ctx context.Context, clientSet *fake.Clientset, handler *kubernetesHandler) informers.SharedInformerFactory {
- // Create a fake replicaset informer and start
- informerFactory := informers.NewSharedInformerFactory(clientSet, 0)
-
- // Add informers
- informerFactory.Apps().V1().Deployments().Informer()
- informerFactory.Apps().V1().ReplicaSets().Informer()
- informerFactory.Core().V1().Pods().Informer()
-
- informerFactory.Start(context.Background().Done())
- informerFactory.WaitForCacheSync(ctx.Done())
- return informerFactory
-}
-
func TestPut(t *testing.T) {
putTests := []struct {
name string
@@ -171,16 +96,16 @@ func TestPut(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
+ clientSet := fake.NewSimpleClientset(tc.in.Resource.CreateResource.Data.(runtime.Object))
handler := kubernetesHandler{
- client: k8sutil.NewFakeKubeClient(nil),
- clientSet: nil,
- deploymentTimeOut: time.Duration(50) * time.Second,
- cacheResyncInterval: time.Duration(1) * time.Second,
+ client: k8sutil.NewFakeKubeClient(nil),
+ deploymentWaiter: &deploymentWaiter{
+ clientSet: clientSet,
+ deploymentTimeOut: time.Duration(50) * time.Second,
+ cacheResyncInterval: time.Duration(1) * time.Second,
+ },
}
- clientSet := fake.NewSimpleClientset(tc.in.Resource.CreateResource.Data.(runtime.Object))
- handler.clientSet = clientSet
-
// If the resource is a deployment, we need to add a replica set to it
if tc.in.Resource.CreateResource.Data.(runtime.Object).GetObjectKind().GroupVersionKind().Kind == "Deployment" {
// The deployment is not marked as ready till we find a replica set. Therefore, we need to create one.
@@ -225,10 +150,12 @@ func TestDelete(t *testing.T) {
}
handler := kubernetesHandler{
- client: k8sutil.NewFakeKubeClient(nil),
- k8sDiscoveryClient: dc,
- deploymentTimeOut: time.Duration(1) * time.Second,
- cacheResyncInterval: time.Duration(10) * time.Second,
+ client: k8sutil.NewFakeKubeClient(nil),
+ k8sDiscoveryClient: dc,
+ deploymentWaiter: &deploymentWaiter{
+ deploymentTimeOut: time.Duration(1) * time.Second,
+ cacheResyncInterval: time.Duration(10) * time.Second,
+ },
}
err := handler.client.Create(ctx, deployment)
@@ -341,996 +268,3 @@ func TestConvertToUnstructured(t *testing.T) {
})
}
}
-
-func TestWaitUntilDeploymentIsReady_NewResource(t *testing.T) {
- ctx := context.Background()
-
- // Create first deployment that will be watched
- deployment := &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Labels: map[string]string{
- kubernetes.LabelManagedBy: kubernetes.LabelManagedByRadiusRP,
- },
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- Spec: v1.DeploymentSpec{
- Replicas: to.Ptr(int32(1)),
- Selector: &metav1.LabelSelector{
- MatchLabels: map[string]string{
- "app": "test",
- },
- },
- },
- Status: v1.DeploymentStatus{
- Conditions: []v1.DeploymentCondition{
- {
- Type: v1.DeploymentProgressing,
- Status: corev1.ConditionTrue,
- Reason: "NewReplicaSetAvailable",
- Message: "Deployment has minimum availability",
- },
- },
- },
- }
-
- clientset := fake.NewSimpleClientset(deployment)
-
- // The deployment is not marked as ready till we find a replica set. Therefore, we need to create one.
- addReplicaSetToDeployment(t, ctx, clientset, deployment)
-
- handler := kubernetesHandler{
- clientSet: clientset,
- deploymentTimeOut: time.Duration(50) * time.Second,
- cacheResyncInterval: time.Duration(10) * time.Second,
- }
-
- err := handler.waitUntilDeploymentIsReady(ctx, deployment)
- require.NoError(t, err, "Failed to wait for deployment to be ready")
-}
-
-func TestWaitUntilDeploymentIsReady_Timeout(t *testing.T) {
- ctx := context.Background()
- // Create first deployment that will be watched
- deployment := &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- Status: v1.DeploymentStatus{
- Conditions: []v1.DeploymentCondition{
- {
- Type: v1.DeploymentProgressing,
- Status: corev1.ConditionFalse,
- Reason: "NewReplicaSetAvailable",
- Message: "Deployment has minimum availability",
- },
- },
- },
- }
-
- deploymentClient := fake.NewSimpleClientset(deployment)
-
- handler := kubernetesHandler{
- clientSet: deploymentClient,
- deploymentTimeOut: time.Duration(1) * time.Second,
- cacheResyncInterval: time.Duration(10) * time.Second,
- }
-
- err := handler.waitUntilDeploymentIsReady(ctx, deployment)
- require.Error(t, err)
- require.Equal(t, "deployment timed out, name: test-deployment, namespace test-namespace, status: Deployment has minimum availability, reason: NewReplicaSetAvailable", err.Error())
-}
-
-func TestWaitUntilDeploymentIsReady_DifferentResourceName(t *testing.T) {
- ctx := context.Background()
- // Create first deployment that will be watched
- deployment := &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- Status: v1.DeploymentStatus{
- Conditions: []v1.DeploymentCondition{
- {
- Type: v1.DeploymentProgressing,
- Status: corev1.ConditionTrue,
- Reason: "NewReplicaSetAvailable",
- Message: "Deployment has minimum availability",
- },
- },
- },
- }
-
- clientset := fake.NewSimpleClientset(deployment)
-
- handler := kubernetesHandler{
- clientSet: clientset,
- deploymentTimeOut: time.Duration(1) * time.Second,
- cacheResyncInterval: time.Duration(10) * time.Second,
- }
-
- err := handler.waitUntilDeploymentIsReady(ctx, &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "not-matched-deployment",
- Namespace: "test-namespace",
- },
- })
-
- // It must be timed out because the name of the deployment does not match.
- require.Error(t, err)
- require.Equal(t, "deployment timed out, name: not-matched-deployment, namespace test-namespace, error occured while fetching latest status: deployments.apps \"not-matched-deployment\" not found", err.Error())
-}
-
-func TestGetPodsInDeployment(t *testing.T) {
- // Create a fake Kubernetes clientset
- fakeClient := fake.NewSimpleClientset()
-
- // Create a Deployment object
- deployment := &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- Spec: v1.DeploymentSpec{
- Selector: &metav1.LabelSelector{
- MatchLabels: map[string]string{
- "app": "test-app",
- },
- },
- },
- }
-
- // Create a ReplicaSet object
- replicaset := &v1.ReplicaSet{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-replicaset",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test-app",
- },
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- UID: "1234",
- },
- }
-
- // Create a Pod object
- pod1 := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test-app",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaset.Name,
- Controller: to.Ptr(true),
- UID: "1234",
- },
- },
- },
- }
-
- // Create a Pod object
- pod2 := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod2",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "doesnotmatch",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: "xyz",
- Controller: to.Ptr(true),
- UID: "1234",
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err := fakeClient.CoreV1().Pods(pod1.Namespace).Create(context.Background(), pod1, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- _, err = fakeClient.CoreV1().Pods(pod2.Namespace).Create(context.Background(), pod2, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create a KubernetesHandler object with the fake clientset
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- ctx := context.Background()
- informerFactory := startInformers(ctx, fakeClient, handler)
-
- // Call the getPodsInDeployment function
- pods, err := handler.getPodsInDeployment(ctx, informerFactory, deployment, replicaset)
- require.NoError(t, err)
- require.Equal(t, 1, len(pods))
- require.Equal(t, pod1.Name, pods[0].Name)
-}
-
-func TestGetCurrentReplicaSetForDeployment(t *testing.T) {
- // Create a fake Kubernetes clientset
- fakeClient := fake.NewSimpleClientset()
-
- // Create a Deployment object
- deployment := &v1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-deployment",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- },
- }
-
- // Create a ReplicaSet object with a higher revision than the other ReplicaSet
- replicaSet1 := &v1.ReplicaSet{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-replicaset-1",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "1"},
- OwnerReferences: []metav1.OwnerReference{
- *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
- Group: v1.SchemeGroupVersion.Group,
- Version: v1.SchemeGroupVersion.Version,
- Kind: "Deployment",
- }),
- },
- },
- }
- // Create another ReplicaSet object with a lower revision than the other ReplicaSet
- replicaSet2 := &v1.ReplicaSet{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-replicaset-2",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "0"},
- OwnerReferences: []metav1.OwnerReference{
- *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
- Group: v1.SchemeGroupVersion.Group,
- Version: v1.SchemeGroupVersion.Version,
- Kind: "Deployment",
- }),
- },
- },
- }
-
- // Create another ReplicaSet object with a higher revision than the other ReplicaSet
- replicaSet3 := &v1.ReplicaSet{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-replicaset-3",
- Namespace: "test-namespace",
- Annotations: map[string]string{"deployment.kubernetes.io/revision": "3"},
- OwnerReferences: []metav1.OwnerReference{
- *metav1.NewControllerRef(deployment, schema.GroupVersionKind{
- Group: v1.SchemeGroupVersion.Group,
- Version: v1.SchemeGroupVersion.Version,
- Kind: "Deployment",
- }),
- },
- },
- }
-
- // Add the ReplicaSet objects to the fake Kubernetes clientset
- _, err := fakeClient.AppsV1().ReplicaSets(replicaSet1.Namespace).Create(context.Background(), replicaSet1, metav1.CreateOptions{})
- require.NoError(t, err)
- _, err = fakeClient.AppsV1().ReplicaSets(replicaSet2.Namespace).Create(context.Background(), replicaSet2, metav1.CreateOptions{})
- require.NoError(t, err)
- _, err = fakeClient.AppsV1().ReplicaSets(replicaSet2.Namespace).Create(context.Background(), replicaSet3, metav1.CreateOptions{})
- require.NoError(t, err)
-
- // Add the Deployment object to the fake Kubernetes clientset
- _, err = fakeClient.AppsV1().Deployments(deployment.Namespace).Create(context.Background(), deployment, metav1.CreateOptions{})
- require.NoError(t, err)
-
- // Create a KubernetesHandler object with the fake clientset
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- ctx := context.Background()
- informerFactory := startInformers(ctx, fakeClient, handler)
-
- // Call the getNewestReplicaSetForDeployment function
- rs := handler.getCurrentReplicaSetForDeployment(ctx, informerFactory, deployment)
- require.Equal(t, replicaSet1.Name, rs.Name)
-}
-
-func TestCheckPodStatus(t *testing.T) {
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod",
- Namespace: "test-namespace",
- },
- Status: corev1.PodStatus{},
- }
-
- podTests := []struct {
- podCondition []corev1.PodCondition
- containerStatus []corev1.ContainerStatus
- isReady bool
- expectedError string
- }{
- {
- // Container is in Terminated state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Terminated: &corev1.ContainerStateTerminated{
- Reason: "Error",
- Message: "Container terminated due to an error",
- },
- },
- },
- },
- isReady: false,
- expectedError: "Container state is 'Terminated' Reason: Error, Message: Container terminated due to an error",
- },
- {
- // Container is in CrashLoopBackOff state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Waiting: &corev1.ContainerStateWaiting{
- Reason: "CrashLoopBackOff",
- Message: "Back-off 5m0s restarting failed container=test-container pod=test-pod",
- },
- },
- },
- },
- isReady: false,
- expectedError: "Container state is 'Waiting' Reason: CrashLoopBackOff, Message: Back-off 5m0s restarting failed container=test-container pod=test-pod",
- },
- {
- // Container is in ErrImagePull state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Waiting: &corev1.ContainerStateWaiting{
- Reason: "ErrImagePull",
- Message: "Cannot pull image",
- },
- },
- },
- },
- isReady: false,
- expectedError: "Container state is 'Waiting' Reason: ErrImagePull, Message: Cannot pull image",
- },
- {
- // Container is in ImagePullBackOff state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Waiting: &corev1.ContainerStateWaiting{
- Reason: "ImagePullBackOff",
- Message: "ImagePullBackOff",
- },
- },
- },
- },
- isReady: false,
- expectedError: "Container state is 'Waiting' Reason: ImagePullBackOff, Message: ImagePullBackOff",
- },
- {
- // No container statuses available
- isReady: false,
- expectedError: "",
- },
- {
- // Container is in Waiting state but not a terminally failed state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Waiting: &corev1.ContainerStateWaiting{
- Reason: "ContainerCreating",
- Message: "Container is being created",
- },
- },
- Ready: false,
- },
- },
- isReady: false,
- expectedError: "",
- },
- {
- // Container's Running state is nil
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Running: nil,
- },
- Ready: false,
- },
- },
- isReady: false,
- expectedError: "",
- },
- {
- // Readiness check is not yet passed
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- Ready: false,
- },
- },
- isReady: false,
- expectedError: "",
- },
- {
- // Container is in Ready state
- podCondition: nil,
- containerStatus: []corev1.ContainerStatus{
- {
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- Ready: true,
- },
- },
- isReady: true,
- expectedError: "",
- },
- }
-
- ctx := context.Background()
- handler := &kubernetesHandler{}
- for _, tc := range podTests {
- pod.Status.Conditions = tc.podCondition
- pod.Status.ContainerStatuses = tc.containerStatus
- isReady, err := handler.checkPodStatus(ctx, pod)
- if tc.expectedError != "" {
- require.Error(t, err)
- require.Equal(t, tc.expectedError, err.Error())
- } else {
- require.NoError(t, err)
- }
- require.Equal(t, tc.isReady, isReady)
- }
-}
-
-func TestCheckAllPodsReady_Success(t *testing.T) {
- // Create a fake Kubernetes clientset
- clientset := fake.NewSimpleClientset()
-
- ctx := context.Background()
-
- _, err := clientset.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
-
- replicaSet := addReplicaSetToDeployment(t, ctx, clientset, testDeployment)
-
- // Create a pod
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- },
- Spec: corev1.PodSpec{
- Containers: []corev1.Container{
- {
- Name: "test-container",
- Image: "test-image",
- },
- },
- },
- Status: corev1.PodStatus{
- Phase: corev1.PodRunning,
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- },
- },
- },
- }
- _, err = clientset.CoreV1().Pods("test-namespace").Create(context.Background(), pod, metav1.CreateOptions{})
- assert.NoError(t, err)
-
- // Create an informer factory and add the deployment and replica set to the cache
- informerFactory := informers.NewSharedInformerFactory(clientset, 0)
- addTestObjects(t, clientset, informerFactory, testDeployment, replicaSet, pod)
-
- // Create a done channel
- doneCh := make(chan error)
-
- // Create a handler with the fake clientset
- handler := &kubernetesHandler{
- clientSet: clientset,
- }
-
- // Call the checkAllPodsReady function
- allReady := handler.checkAllPodsReady(ctx, informerFactory, testDeployment, replicaSet, doneCh)
-
- // Check that all pods are ready
- require.True(t, allReady)
-}
-
-func TestCheckAllPodsReady_Fail(t *testing.T) {
- // Create a fake Kubernetes clientset
- clientset := fake.NewSimpleClientset()
-
- ctx := context.Background()
-
- _, err := clientset.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
-
- replicaSet := addReplicaSetToDeployment(t, ctx, clientset, testDeployment)
-
- // Create a pod
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaSet.Name,
- Controller: to.Ptr(true),
- },
- },
- },
- Spec: corev1.PodSpec{
- Containers: []corev1.Container{
- {
- Name: "test-container",
- Image: "test-image",
- },
- },
- },
- Status: corev1.PodStatus{
- Phase: corev1.PodRunning,
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: false,
- },
- },
- },
- }
- _, err = clientset.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{})
- require.NoError(t, err)
-
- // Create an informer factory and add the deployment and replica set to the cache
- informerFactory := informers.NewSharedInformerFactory(clientset, 0)
- addTestObjects(t, clientset, informerFactory, testDeployment, replicaSet, pod)
-
- // Create a done channel
- doneCh := make(chan error)
-
- // Create a handler with the fake clientset
- handler := &kubernetesHandler{
- clientSet: clientset,
- }
-
- // Call the checkAllPodsReady function
- allReady := handler.checkAllPodsReady(ctx, informerFactory, testDeployment, replicaSet, doneCh)
-
- // Check that all pods are ready
- require.False(t, allReady)
-}
-
-func TestCheckDeploymentStatus_AllReady(t *testing.T) {
- // Create a fake Kubernetes fakeClient
- fakeClient := fake.NewSimpleClientset()
-
- ctx := context.Background()
- _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
- replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
-
- // Create a Pod object
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaSet.Name,
- Controller: to.Ptr(true),
- },
- },
- },
- Status: corev1.PodStatus{
- Conditions: []corev1.PodCondition{
- {
- Type: corev1.PodScheduled,
- Status: corev1.ConditionTrue,
- },
- },
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create an informer factory and add the deployment to the cache
- informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
- addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
-
- // Create a fake item and object
- item := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "metadata": map[string]interface{}{
- "name": "test-deployment",
- "namespace": "test-namespace",
- },
- },
- }
-
- // Create a done channel
- doneCh := make(chan error, 1)
-
- // Call the checkDeploymentStatus function
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- go handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
-
- err = <-doneCh
-
- // Check that the deployment readiness was checked
- require.Nil(t, err)
-}
-
-func TestCheckDeploymentStatus_NoReplicaSetsFound(t *testing.T) {
- // Create a fake Kubernetes fakeClient
- fakeClient := fake.NewSimpleClientset()
-
- ctx := context.Background()
- _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
-
- // Create a Pod object
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- },
- Status: corev1.PodStatus{
- Conditions: []corev1.PodCondition{
- {
- Type: corev1.PodScheduled,
- Status: corev1.ConditionTrue,
- },
- },
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create an informer factory and add the deployment to the cache
- informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
- err = informerFactory.Apps().V1().Deployments().Informer().GetIndexer().Add(testDeployment)
- require.NoError(t, err, "Failed to add deployment to informer cache")
- err = informerFactory.Core().V1().Pods().Informer().GetIndexer().Add(pod)
- require.NoError(t, err, "Failed to add pod to informer cache")
- // Note: No replica set added
-
- // Create a fake item and object
- item := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "metadata": map[string]interface{}{
- "name": "test-deployment",
- "namespace": "test-namespace",
- },
- },
- }
-
- // Create a done channel
- doneCh := make(chan error, 1)
-
- // Call the checkDeploymentStatus function
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- allReady := handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
-
- // Check that the deployment readiness was checked
- require.False(t, allReady)
-}
-
-func TestCheckDeploymentStatus_PodsNotReady(t *testing.T) {
- // Create a fake Kubernetes fakeClient
- fakeClient := fake.NewSimpleClientset()
-
- ctx := context.Background()
- _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
- replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
-
- // Create a Pod object
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaSet.Name,
- Controller: to.Ptr(true),
- },
- },
- },
- Status: corev1.PodStatus{
- Conditions: []corev1.PodCondition{
- {
- Type: corev1.PodScheduled,
- Status: corev1.ConditionTrue,
- },
- },
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- State: corev1.ContainerState{
- Terminated: &corev1.ContainerStateTerminated{
- Reason: "Error",
- Message: "Container terminated due to an error",
- },
- },
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create an informer factory and add the deployment to the cache
- informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
- addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
-
- // Create a fake item and object
- item := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "metadata": map[string]interface{}{
- "name": "test-deployment",
- "namespace": "test-namespace",
- },
- },
- }
-
- // Create a done channel
- doneCh := make(chan error, 1)
-
- // Call the checkDeploymentStatus function
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- go handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
- err = <-doneCh
-
- // Check that the deployment readiness was checked
- require.Error(t, err)
- require.Equal(t, err.Error(), "Container state is 'Terminated' Reason: Error, Message: Container terminated due to an error")
-}
-
-func TestCheckDeploymentStatus_ObservedGenerationMismatch(t *testing.T) {
- // Modify testDeployment to have a different generation than the observed generation
- testDeployment.Generation = 2
-
- // Create a fake Kubernetes fakeClient
- fakeClient := fake.NewSimpleClientset()
-
- ctx := context.Background()
- _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
- replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
-
- // Create a Pod object
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaSet.Name,
- Controller: to.Ptr(true),
- },
- },
- },
- Status: corev1.PodStatus{
- Conditions: []corev1.PodCondition{
- {
- Type: corev1.PodScheduled,
- Status: corev1.ConditionTrue,
- },
- },
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create an informer factory and add the deployment to the cache
- informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
- addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
-
- // Create a fake item and object
- item := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "metadata": map[string]interface{}{
- "name": "test-deployment",
- "namespace": "test-namespace",
- },
- },
- }
-
- // Create a done channel
- doneCh := make(chan error, 1)
-
- // Call the checkDeploymentStatus function
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
-
- // Check that the deployment readiness was checked
- require.Zero(t, len(doneCh))
-}
-
-func TestCheckDeploymentStatus_DeploymentNotProgressing(t *testing.T) {
- // Create a fake Kubernetes fakeClient
- fakeClient := fake.NewSimpleClientset()
-
- ctx := context.Background()
- _, err := fakeClient.AppsV1().Deployments("test-namespace").Create(ctx, testDeployment, metav1.CreateOptions{})
- require.NoError(t, err)
- replicaSet := addReplicaSetToDeployment(t, ctx, fakeClient, testDeployment)
-
- // Create a Pod object
- pod := &corev1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-pod1",
- Namespace: "test-namespace",
- Labels: map[string]string{
- "app": "test",
- },
- OwnerReferences: []metav1.OwnerReference{
- {
- Kind: "ReplicaSet",
- Name: replicaSet.Name,
- Controller: to.Ptr(true),
- },
- },
- },
- Status: corev1.PodStatus{
- Conditions: []corev1.PodCondition{
- {
- Type: corev1.PodScheduled,
- Status: corev1.ConditionTrue,
- },
- },
- ContainerStatuses: []corev1.ContainerStatus{
- {
- Name: "test-container",
- Ready: true,
- State: corev1.ContainerState{
- Running: &corev1.ContainerStateRunning{},
- },
- },
- },
- },
- }
-
- // Add the Pod object to the fake Kubernetes clientset
- _, err = fakeClient.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{})
- require.NoError(t, err, "Failed to create Pod: %v", err)
-
- // Create an informer factory and add the deployment to the cache
- informerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
- addTestObjects(t, fakeClient, informerFactory, testDeployment, replicaSet, pod)
-
- testDeployment.Status = v1.DeploymentStatus{
- Conditions: []v1.DeploymentCondition{
- {
- Type: v1.DeploymentProgressing,
- Status: corev1.ConditionFalse,
- Reason: "NewReplicaSetAvailable",
- Message: "Deployment has minimum availability",
- },
- },
- }
-
- // Create a fake item and object
- item := &unstructured.Unstructured{
- Object: map[string]interface{}{
- "metadata": map[string]interface{}{
- "name": "test-deployment",
- "namespace": "test-namespace",
- },
- },
- }
-
- // Create a done channel
- doneCh := make(chan error, 1)
-
- // Call the checkDeploymentStatus function
- handler := &kubernetesHandler{
- clientSet: fakeClient,
- }
-
- ready := handler.checkDeploymentStatus(ctx, informerFactory, item, doneCh)
- require.False(t, ready)
-}
-
-func addTestObjects(t *testing.T, fakeClient *fake.Clientset, informerFactory informers.SharedInformerFactory, deployment *v1.Deployment, replicaSet *v1.ReplicaSet, pod *corev1.Pod) {
- err := informerFactory.Apps().V1().Deployments().Informer().GetIndexer().Add(testDeployment)
- require.NoError(t, err, "Failed to add deployment to informer cache")
- err = informerFactory.Apps().V1().ReplicaSets().Informer().GetIndexer().Add(replicaSet)
- require.NoError(t, err, "Failed to add replica set to informer cache")
- err = informerFactory.Core().V1().Pods().Informer().GetIndexer().Add(pod)
- require.NoError(t, err, "Failed to add pod to informer cache")
-}
diff --git a/pkg/corerp/model/application_model.go b/pkg/corerp/model/application_model.go
index 3f74b837d3..bd86257eaf 100644
--- a/pkg/corerp/model/application_model.go
+++ b/pkg/corerp/model/application_model.go
@@ -36,6 +36,7 @@ import (
resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes"
"k8s.io/client-go/discovery"
+ "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -48,7 +49,7 @@ const (
// NewApplicationModel configures RBAC support on connections based on connection kind, configures the providers supported by the appmodel,
// registers the renderers and handlers for various resources, and checks for duplicate registrations.
-func NewApplicationModel(arm *armauth.ArmConfig, k8sClient client.Client, k8sClientSet kubernetes.Interface, discoveryClient discovery.ServerResourcesInterface) (ApplicationModel, error) {
+func NewApplicationModel(arm *armauth.ArmConfig, k8sClient client.Client, k8sClientSet kubernetes.Interface, discoveryClient discovery.ServerResourcesInterface, k8sDynamicClientSet dynamic.Interface) (ApplicationModel, error) {
// Configure RBAC support on connections based connection kind.
// Role names can be user input or default roles assigned by Radius.
// Leave RoleNames field empty if no default roles are supported for a connection kind.
@@ -116,7 +117,7 @@ func NewApplicationModel(arm *armauth.ArmConfig, k8sClient client.Client, k8sCli
Type: AnyResourceType,
Provider: resourcemodel.ProviderKubernetes,
},
- ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient),
+ ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient, k8sDynamicClientSet),
},
{
ResourceType: resourcemodel.ResourceType{
@@ -124,7 +125,7 @@ func NewApplicationModel(arm *armauth.ArmConfig, k8sClient client.Client, k8sCli
Provider: resourcemodel.ProviderKubernetes,
},
ResourceTransformer: azcontainer.TransformSecretProviderClass,
- ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient),
+ ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient, k8sDynamicClientSet),
},
{
ResourceType: resourcemodel.ResourceType{
@@ -132,7 +133,7 @@ func NewApplicationModel(arm *armauth.ArmConfig, k8sClient client.Client, k8sCli
Provider: resourcemodel.ProviderKubernetes,
},
ResourceTransformer: azcontainer.TransformFederatedIdentitySA,
- ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient),
+ ResourceHandler: handlers.NewKubernetesHandler(k8sClient, k8sClientSet, discoveryClient, k8sDynamicClientSet),
},
}
diff --git a/pkg/corerp/renderers/gateway/render.go b/pkg/corerp/renderers/gateway/render.go
index 8203328292..2fe79fa094 100644
--- a/pkg/corerp/renderers/gateway/render.go
+++ b/pkg/corerp/renderers/gateway/render.go
@@ -103,7 +103,7 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
publicEndpoint = getPublicEndpoint(hostname, options.Environment.Gateway.Port, isHttps)
}
- gatewayObject, err := MakeGateway(ctx, options, gateway, gateway.Name, applicationName, hostname)
+ gatewayObject, err := MakeRootHTTPProxy(ctx, options, gateway, gateway.Name, applicationName, hostname)
if err != nil {
return renderers.RendererOutput{}, err
}
@@ -116,7 +116,7 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
},
}
- httpRouteObjects, err := MakeHttpRoutes(ctx, options, *gateway, &gateway.Properties, gatewayName, applicationName)
+ httpRouteObjects, err := MakeRoutesHTTPProxies(ctx, options, *gateway, &gateway.Properties, gatewayName, gatewayObject, applicationName)
if err != nil {
return renderers.RendererOutput{}, err
}
@@ -128,9 +128,9 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
}, nil
}
-// MakeGateway validates the Gateway resource and its dependencies, and creates a Contour HTTPProxy resource
+// MakeRootHTTPProxy validates the Gateway resource and its dependencies, and creates a Contour HTTPProxy resource
// to act as the Gateway.
-func MakeGateway(ctx context.Context, options renderers.RenderOptions, gateway *datamodel.Gateway, resourceName string, applicationName string, hostname string) (rpv1.OutputResource, error) {
+func MakeRootHTTPProxy(ctx context.Context, options renderers.RenderOptions, gateway *datamodel.Gateway, resourceName string, applicationName string, hostname string) (rpv1.OutputResource, error) {
includes := []contourv1.Include{}
dependencies := options.Dependencies
@@ -292,9 +292,9 @@ func MakeGateway(ctx context.Context, options renderers.RenderOptions, gateway *
return rpv1.NewKubernetesOutputResource(rpv1.LocalIDGateway, rootHTTPProxy, rootHTTPProxy.ObjectMeta), nil
}
-// MakeHttpRoutes creates HTTPProxy objects for each route in the gateway and returns them as OutputResources. It returns
+// MakeRoutesHTTPProxies creates HTTPProxy objects for each route in the gateway and returns them as OutputResources. It returns
// an error if it fails to get the route name.
-func MakeHttpRoutes(ctx context.Context, options renderers.RenderOptions, resource datamodel.Gateway, gateway *datamodel.GatewayProperties, gatewayName string, applicationName string) ([]rpv1.OutputResource, error) {
+func MakeRoutesHTTPProxies(ctx context.Context, options renderers.RenderOptions, resource datamodel.Gateway, gateway *datamodel.GatewayProperties, gatewayName string, gatewayOutPutResource rpv1.OutputResource, applicationName string) ([]rpv1.OutputResource, error) {
dependencies := options.Dependencies
objects := make(map[string]*contourv1.HTTPProxy)
@@ -387,6 +387,9 @@ func MakeHttpRoutes(ctx context.Context, options renderers.RenderOptions, resour
}
objects[localID] = httpProxyObject
+
+ // Add the route as a dependency of the root http proxy to ensure that the route is created before the root http proxy
+ gatewayOutPutResource.CreateResource.Dependencies = append(gatewayOutPutResource.CreateResource.Dependencies, localID)
}
var outputResources []rpv1.OutputResource
diff --git a/pkg/corerp/renderers/gateway/render_test.go b/pkg/corerp/renderers/gateway/render_test.go
index 204f75b041..dab84c49f1 100644
--- a/pkg/corerp/renderers/gateway/render_test.go
+++ b/pkg/corerp/renderers/gateway/render_test.go
@@ -19,6 +19,7 @@ package gateway
import (
"context"
"fmt"
+ "strings"
"testing"
contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1"
@@ -1466,6 +1467,12 @@ func validateHTTPProxy(t *testing.T, outputResources []rpv1.OutputResource, expe
httpProxy, httpProxyOutputResource := kubernetes.FindContourHTTPProxy(outputResources)
expectedHTTPProxyOutputResource := rpv1.NewKubernetesOutputResource(rpv1.LocalIDGateway, httpProxy, httpProxy.ObjectMeta)
+ for _, r := range outputResources {
+ if strings.Contains(r.LocalID, rpv1.LocalIDHttpRoute) {
+ expectedHTTPProxyOutputResource.CreateResource.Dependencies = append(expectedHTTPProxyOutputResource.CreateResource.Dependencies, r.LocalID)
+ }
+ }
+
require.Equal(t, expectedHTTPProxyOutputResource, httpProxyOutputResource)
require.Equal(t, kubernetes.NormalizeResourceName(resourceName), httpProxy.Name)
require.Equal(t, applicationName, httpProxy.Namespace)
diff --git a/test/functional/shared/resources/gateway_test.go b/test/functional/shared/resources/gateway_test.go
index ab6d45f006..647a3a9317 100644
--- a/test/functional/shared/resources/gateway_test.go
+++ b/test/functional/shared/resources/gateway_test.go
@@ -31,6 +31,7 @@ import (
"github.com/radius-project/radius/test/step"
"github.com/radius-project/radius/test/validation"
"github.com/stretchr/testify/require"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
@@ -107,6 +108,8 @@ func Test_Gateway(t *testing.T) {
require.NoError(t, err)
t.Logf("found root proxy with hostname: {%s} and status: {%s}", metadata.Hostname, metadata.Status)
+ require.Equal(t, "Valid HTTPProxy", metadata.Status)
+
// Set up pod port-forwarding for contour-envoy
t.Logf("Setting up portforward")
@@ -369,6 +372,50 @@ func Test_Gateway_TLSTermination(t *testing.T) {
test.Test(t)
}
+func Test_Gateway_Failure(t *testing.T) {
+ template := "testdata/corerp-resources-gateway-failure.bicep"
+ name := "corerp-resources-gateway-failure"
+ secret := "secret"
+
+ // We might see either of these states depending on the timing.
+ validateFn := step.ValidateAnyDetails("DeploymentFailed", []step.DeploymentErrorDetail{
+ {
+ Code: "ResourceDeploymentFailure",
+ Details: []step.DeploymentErrorDetail{
+ {
+ Code: "Internal",
+ MessageContains: "invalid TLS certificate",
+ },
+ },
+ },
+ })
+
+ test := shared.NewRPTest(t, name, []shared.TestStep{
+ {
+ Executor: step.NewDeployErrorExecutor(template, validateFn),
+ SkipObjectValidation: true,
+ SkipKubernetesOutputResourceValidation: true,
+ },
+ },
+ unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]interface{}{
+ "name": secret,
+ "namespace": "mynamespace",
+ },
+ "type": "Opaque",
+ "data": map[string]interface{}{
+ "tls.crt": "",
+ "tls.key": "",
+ },
+ },
+ })
+
+ test.Test(t)
+}
+
func testGatewayWithPortForward(t *testing.T, ctx context.Context, at shared.RPTest, hostname string, remotePort int, isHttps bool, tests []GatewayTestConfig) error {
// stopChan will close the port-forward connection on close
stopChan := make(chan struct{})
diff --git a/test/functional/shared/resources/testdata/corerp-resources-gateway-failure.bicep b/test/functional/shared/resources/testdata/corerp-resources-gateway-failure.bicep
new file mode 100644
index 0000000000..ef7a22b8a5
--- /dev/null
+++ b/test/functional/shared/resources/testdata/corerp-resources-gateway-failure.bicep
@@ -0,0 +1,54 @@
+import radius as radius
+
+@description('ID of the Radius environment. Passed in automatically via the rad CLI')
+param environment string
+
+resource demoApplication 'Applications.Core/applications@2022-03-15-privatepreview' = {
+ name: 'corerp-resources-gateway-failure-app'
+ properties: {
+ environment: environment
+ }
+}
+
+resource demoSecretStore 'Applications.Core/secretStores@2022-03-15-privatepreview' = {
+ name: 'corerp-resources-gateway-failure-secretstore'
+ properties: {
+ application: demoApplication.id
+ type: 'certificate'
+
+ // Reference the existing mynamespace/secret Kubernetes secret
+ resource: 'mynamespace/secret'
+ data: {
+ // Make the tls.crt and tls.key secrets available to the application
+ 'tls.crt': {}
+ 'tls.key': {}
+ }
+ }
+}
+
+resource demoGateway 'Applications.Core/gateways@2022-03-15-privatepreview' = {
+ name: 'corerp-resources-gateway-failure-gateway'
+ properties: {
+ application: demoApplication.id
+ hostname: {
+ fullyQualifiedHostname: 'a.example.com' // Replace with your domain name.
+ }
+ routes: [
+ {
+ path: '/'
+ destination: demoRoute.id
+ }
+ ]
+ tls: {
+ certificateFrom: demoSecretStore.id
+ minimumProtocolVersion: '1.2'
+ }
+ }
+}
+
+resource demoRoute 'Applications.Core/httpRoutes@2022-03-15-privatepreview' = {
+ name: 'corerp-resources-gateway-failure-route'
+ properties: {
+ application: demoApplication.id
+ }
+}
diff --git a/test/functional/shared/rptest.go b/test/functional/shared/rptest.go
index 0f93500c4e..42a2865532 100644
--- a/test/functional/shared/rptest.go
+++ b/test/functional/shared/rptest.go
@@ -163,6 +163,9 @@ func (ct RPTest) CreateInitialResources(ctx context.Context) error {
}
for _, r := range ct.InitialResources {
+ if err := kubernetes.EnsureNamespace(ctx, ct.Options.K8sClient, r.GetNamespace()); err != nil {
+ return fmt.Errorf("failed to create namespace %s: %w", ct.Name, err)
+ }
if err := ct.Options.Client.Create(ctx, &r); err != nil {
return fmt.Errorf("failed to create resource %#v: %w", r, err)
}
From 512a08bd457bf6ab0d4ad1b10f9ee4fc8a48a8c6 Mon Sep 17 00:00:00 2001
From: Ryan Nowak
Date: Wed, 6 Sep 2023 13:32:44 -0700
Subject: [PATCH 03/13] Add plumbing for tracked resources (#6199)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
This change adds the API endpoint for listing resources in a resource
group. Right now nothing *writes* data to this collection, so nothing
will be returned. The write-side of this relationship will be sent in a
follow-up PR.
## Type of change
- This pull request adds or changes features of Radius and has an
approved issue (issue link required).
## Auto-generated summary
### ๐ค Generated by Copilot at db58a8f
### Summary
๐๐๐ฆ
This pull request adds the `ResourceGroups.Resources` operation to the
UCP resource manager API, which allows listing the tracked resources in
a UCP resource group. It also adds the necessary models, converters,
tests, and documentation for the operation, and updates the frontend
routes and the plane name references. It removes the unused
`pkg/ucp/datamodel/resource.go` file.
> _`ResourceGroups` API_
> _Listing tracked resources_
> _Autumn harvest time_
### Walkthrough
* Add the `ResourceGroups.Resources` operation to the UCP API, which
lists the resources in a UCP resource group
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-cc310274506998a9cb634be1fcf47f3d60e3952ededc2877d22e13aa6b1da6a9R109-R119),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-4b6a62f07b9f16f76cad4ff01bc3123c05785d1609f10a0f65ca4587b1477380R644-R690),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-c8b433357e1b615716c00d5c04690a6743c6f0dd73f772da9a981c86bd509c5eR102-R110),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-9abfb8c07367e0af734a41a615f83d1812ff44b5433f3aed6fbae234bb31e7b0R1-R110),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-6a0e13310a26b0bc0209469890851dd4afbc44bd718a77f51ec68e4f4d4170b5R1-R144),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-0c748400ed77b4a219c97b84d9ecbc73d081ca60a4e3abaa34cc9617eeb9e5abR273-R331),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-c74c23d29da9a5ac2ea4388c4cfd23f42018e45c93a29f71c4c5eeadebdfa4c6R126-R131),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-416356e403db5b572d2c0f372baef2abfeb3000363efc6d41f55bdc65f47ebafR116-R120))
* Add the `TrackedResourceList` and `TrackedResourceListEntry` types to
the UCP API and data model, which represent the resources stored in a
UCP resource group
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-cc310274506998a9cb634be1fcf47f3d60e3952ededc2877d22e13aa6b1da6a9R56-R71),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-4b6a62f07b9f16f76cad4ff01bc3123c05785d1609f10a0f65ca4587b1477380R1702-R1740),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-6f7e7aae5544d15eeb60fd6452c8cdcdd56c68c1c6c5a9910511ef0aef14aa77R386-R403),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-6f300088351b430ca2464beef84ad3069228f02d95a3dcd3641926ad1c2620fbR963-R1024),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-175a83221c86f8341e51bd539a67049bd60b6300f86d403e5c3d0b5e426e2e4dR1-R55),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-1d3c43c905868be0c590c7d498d5f3349ca187ab61dc75417f27005fc00d6a1dR1-R48),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-67d1d73e44007a6f222129ec3b59a86b35e0a0b819d43c714f46475dfccbc1f6R1-R47))
* Add the `UcpResourceActionSync` type to the UCP API, which defines a
custom action type for UCP resources
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-49dd7f9c188a21384e7a8f64fbf861fa27bdc362c4bc738f1e74e62820aefe4fR160-R168))
* Add the test data, test files, and comments for the new types and
operations
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-5cafe7e061f2893eb4c59ad4a73e7ab07fa5031b59ebee00fda2810595369e36L1-R21),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-134cfc7ecd8473c79defbf93680b83f13d844fef7ad68d994b7c89f87a7c3faeR1-R73),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-cc76a2a14994ce1ed06bd12ba9665a1d20a17992f345a7f8ca06afc934da2a92L35-R35))
* Update the comments for the catch-all paths for the proxy requests to
use the correct plane name
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-c8b433357e1b615716c00d5c04690a6743c6f0dd73f772da9a981c86bd509c5eL107-R116),
[link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-c8b433357e1b615716c00d5c04690a6743c6f0dd73f772da9a981c86bd509c5eL114-R123))
* Delete the unused file `pkg/ucp/datamodel/resource.go`
([link](https://github.com/radius-project/radius/pull/6199/files?diff=unified&w=0#diff-333442dc1efebe43776ff8af54908f266a90d118599279b0d6f065f81673bb19))
---
build/generate.mk | 2 +-
.../genericresource_conversion.go | 49 ++++++
.../genericresource_conversion_test.go | 73 ++++++++
.../testdata/genericresource_datamodel.json | 22 +++
.../zz_generated_client_factory.go | 5 +
.../zz_generated_models.go | 62 +++++++
.../zz_generated_models_serde.go | 156 ++++++++++++++++++
.../zz_generated_options.go | 5 +
.../zz_generated_resources_client.go | 108 ++++++++++++
.../zz_generated_response_types.go | 6 +
.../converter/genericresource_converter.go | 47 ++++++
pkg/ucp/datamodel/genericresource.go | 55 ++++++
pkg/ucp/datamodel/resource.go | 29 ----
.../resourcegroups/listresources.go | 110 ++++++++++++
.../resourcegroups/listresources_test.go | 144 ++++++++++++++++
pkg/ucp/frontend/radius/routes.go | 13 +-
.../examples/Resources_List.json | 30 ++++
.../2022-09-01-privatepreview/openapi.json | 126 ++++++++++++++
.../Resources_List.json | 30 ++++
typespec/UCP/resourcegroups.tsp | 20 +++
20 files changed, 1060 insertions(+), 32 deletions(-)
create mode 100644 pkg/ucp/api/v20220901privatepreview/genericresource_conversion.go
create mode 100644 pkg/ucp/api/v20220901privatepreview/genericresource_conversion_test.go
create mode 100644 pkg/ucp/api/v20220901privatepreview/testdata/genericresource_datamodel.json
create mode 100644 pkg/ucp/api/v20220901privatepreview/zz_generated_resources_client.go
create mode 100644 pkg/ucp/datamodel/converter/genericresource_converter.go
create mode 100644 pkg/ucp/datamodel/genericresource.go
delete mode 100644 pkg/ucp/datamodel/resource.go
create mode 100644 pkg/ucp/frontend/controller/resourcegroups/listresources.go
create mode 100644 pkg/ucp/frontend/controller/resourcegroups/listresources_test.go
create mode 100644 swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Resources_List.json
create mode 100644 typespec/UCP/examples/2022-09-01-privatepreview/Resources_List.json
diff --git a/build/generate.mk b/build/generate.mk
index f7417df5b6..3863d7b2fe 100644
--- a/build/generate.mk
+++ b/build/generate.mk
@@ -32,7 +32,7 @@ generate-tsp-installed:
@echo "$(ARROW) OK"
.PHONY: generate-openapi-spec
-generate-openapi-spec:
+generate-openapi-spec: # Generates all Radius OpenAPI specs from TypeSpec.
@echo "Generating openapi specs from typespec models."
cd typespec/UCP && npx$(CMD_EXT) tsp compile .
cd typespec/Applications.Core && npx$(CMD_EXT) tsp compile .
diff --git a/pkg/ucp/api/v20220901privatepreview/genericresource_conversion.go b/pkg/ucp/api/v20220901privatepreview/genericresource_conversion.go
new file mode 100644
index 0000000000..089cbaade7
--- /dev/null
+++ b/pkg/ucp/api/v20220901privatepreview/genericresource_conversion.go
@@ -0,0 +1,49 @@
+/*
+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 v20220901privatepreview
+
+import (
+ "errors"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ "github.com/radius-project/radius/pkg/to"
+ "github.com/radius-project/radius/pkg/ucp/datamodel"
+)
+
+const (
+ ResourceType = "System.Resources/resources"
+)
+
+// ConvertTo converts from the versioned GenericResource resource to version-agnostic datamodel.
+func (src *GenericResource) ConvertTo() (v1.DataModelInterface, error) {
+ return nil, errors.New("the GenericResource type does not support conversion from versioned models")
+}
+
+// ConvertFrom converts from version-agnostic datamodel to the versioned GenericResource resource.
+func (dst *GenericResource) ConvertFrom(src v1.DataModelInterface) error {
+ entry, ok := src.(*datamodel.GenericResource)
+ if !ok {
+ return v1.ErrInvalidModelConversion
+ }
+
+ // The properties are used to store the data of the "tracked" resource.
+ dst.ID = to.Ptr(entry.Properties.ID)
+ dst.Name = to.Ptr(entry.Properties.Name)
+ dst.Type = to.Ptr(entry.Properties.Type)
+
+ return nil
+}
diff --git a/pkg/ucp/api/v20220901privatepreview/genericresource_conversion_test.go b/pkg/ucp/api/v20220901privatepreview/genericresource_conversion_test.go
new file mode 100644
index 0000000000..e1a93743ff
--- /dev/null
+++ b/pkg/ucp/api/v20220901privatepreview/genericresource_conversion_test.go
@@ -0,0 +1,73 @@
+/*
+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 v20220901privatepreview
+
+import (
+ "encoding/json"
+ "errors"
+ "testing"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/radius-project/radius/pkg/ucp/datamodel"
+ "github.com/radius-project/radius/test/testutil"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_GenericResource_VersionedToDataModel(t *testing.T) {
+ versioned := &GenericResource{}
+ dm, err := versioned.ConvertTo()
+ require.Equal(t, errors.New("the GenericResource type does not support conversion from versioned models"), err)
+ require.Nil(t, dm)
+}
+
+func Test_GenericResource_DataModelToVersioned(t *testing.T) {
+ conversionTests := []struct {
+ filename string
+ expected *GenericResource
+ err error
+ }{
+ {
+ filename: "genericresource_datamodel.json",
+ expected: &GenericResource{
+ ID: to.Ptr("/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/applications/test-app"),
+ Type: to.Ptr("Applications.Core/applications"),
+ Name: to.Ptr("test-app"),
+ },
+ },
+ }
+
+ for _, tt := range conversionTests {
+ t.Run(tt.filename, func(t *testing.T) {
+ rawPayload := testutil.ReadFixture(tt.filename)
+ data := &datamodel.GenericResource{}
+ err := json.Unmarshal(rawPayload, data)
+ require.NoError(t, err)
+
+ versioned := &GenericResource{}
+
+ err = versioned.ConvertFrom(data)
+
+ if tt.err != nil {
+ require.ErrorIs(t, err, tt.err)
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, tt.expected, versioned)
+ }
+ })
+ }
+}
diff --git a/pkg/ucp/api/v20220901privatepreview/testdata/genericresource_datamodel.json b/pkg/ucp/api/v20220901privatepreview/testdata/genericresource_datamodel.json
new file mode 100644
index 0000000000..c8fabfc130
--- /dev/null
+++ b/pkg/ucp/api/v20220901privatepreview/testdata/genericresource_datamodel.json
@@ -0,0 +1,22 @@
+{
+ "id": "/planes/radius/local/providers/System.Resources/resources/asdf",
+ "name": "asdf",
+ "type": "System.Resources/resources",
+ "location": "global",
+ "systemData": {
+ "createdBy": "fakeid@live.com",
+ "createdByType": "User",
+ "createdAt": "2021-09-24T19:09:54.2403864Z",
+ "lastModifiedBy": "fakeid@live.com",
+ "lastModifiedByType": "User",
+ "lastModifiedAt": "2021-09-24T20:09:54.2403864Z"
+ },
+ "tags": {
+ "env": "dev"
+ },
+ "properties": {
+ "id": "/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/applications/test-app",
+ "type": "Applications.Core/applications",
+ "name": "test-app"
+ }
+}
\ No newline at end of file
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_client_factory.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_client_factory.go
index f0fbcdc587..c88e54ed34 100644
--- a/pkg/ucp/api/v20220901privatepreview/zz_generated_client_factory.go
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_client_factory.go
@@ -54,3 +54,8 @@ func (c *ClientFactory) NewResourceGroupsClient() *ResourceGroupsClient {
return subClient
}
+func (c *ClientFactory) NewResourcesClient() *ResourcesClient {
+ subClient, _ := NewResourcesClient(c.credential, c.options)
+ return subClient
+}
+
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_models.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_models.go
index eebab52ecf..0a2ddfb425 100644
--- a/pkg/ucp/api/v20220901privatepreview/zz_generated_models.go
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_models.go
@@ -167,6 +167,25 @@ func (a *AzureServicePrincipalProperties) GetAzureCredentialProperties() *AzureC
}
}
+// ComponentsKhmx01SchemasGenericresourceAllof0 - Concrete proxy resource types can be created by aliasing this type using
+// a specific property type.
+type ComponentsKhmx01SchemasGenericresourceAllof0 struct {
+ // The resource-specific properties for this resource.
+ Properties map[string]any
+
+ // READ-ONLY; Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
+ ID *string
+
+ // READ-ONLY; The name of the resource
+ Name *string
+
+ // READ-ONLY; Azure Resource Manager metadata containing createdBy and modifiedBy information.
+ SystemData *SystemData
+
+ // READ-ONLY; The type of the resource. E.g. "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts"
+ Type *string
+}
+
// CredentialStorageProperties - The base credential storage properties
type CredentialStorageProperties struct {
// REQUIRED; The kind of credential storage
@@ -210,6 +229,33 @@ type ErrorResponse struct {
Error *ErrorDetail
}
+// GenericResource - Represents resource data.
+type GenericResource struct {
+ // The resource-specific properties for this resource.
+ Properties map[string]any
+
+ // READ-ONLY; The name of resource
+ Name *string
+
+ // READ-ONLY; Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
+ ID *string
+
+ // READ-ONLY; Azure Resource Manager metadata containing createdBy and modifiedBy information.
+ SystemData *SystemData
+
+ // READ-ONLY; The type of the resource. E.g. "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts"
+ Type *string
+}
+
+// GenericResourceListResult - The response of a GenericResource list operation.
+type GenericResourceListResult struct {
+ // REQUIRED; The GenericResource items on this page
+ Value []*GenericResource
+
+ // The link to the next page of items
+ NextLink *string
+}
+
// InternalCredentialStorageProperties - Internal credential storage properties
type InternalCredentialStorageProperties struct {
// REQUIRED; The kind of credential storage
@@ -280,6 +326,22 @@ type PlaneResourceTagsUpdate struct {
Tags map[string]*string
}
+// ProxyResource - The resource model definition for a Azure Resource Manager proxy resource. It will not have tags and a
+// location
+type ProxyResource struct {
+ // READ-ONLY; Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
+ ID *string
+
+ // READ-ONLY; The name of the resource
+ Name *string
+
+ // READ-ONLY; Azure Resource Manager metadata containing createdBy and modifiedBy information.
+ SystemData *SystemData
+
+ // READ-ONLY; The type of the resource. E.g. "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts"
+ Type *string
+}
+
// Resource - Common fields that are returned in the response for all Azure Resource Manager resources
type Resource struct {
// READ-ONLY; Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_models_serde.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_models_serde.go
index 8709f69fad..ac9308dbde 100644
--- a/pkg/ucp/api/v20220901privatepreview/zz_generated_models_serde.go
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_models_serde.go
@@ -384,6 +384,49 @@ func (a *AzureServicePrincipalProperties) UnmarshalJSON(data []byte) error {
return nil
}
+// MarshalJSON implements the json.Marshaller interface for type ComponentsKhmx01SchemasGenericresourceAllof0.
+func (c ComponentsKhmx01SchemasGenericresourceAllof0) MarshalJSON() ([]byte, error) {
+ objectMap := make(map[string]any)
+ populate(objectMap, "id", c.ID)
+ populate(objectMap, "name", c.Name)
+ populate(objectMap, "properties", c.Properties)
+ populate(objectMap, "systemData", c.SystemData)
+ populate(objectMap, "type", c.Type)
+ return json.Marshal(objectMap)
+}
+
+// UnmarshalJSON implements the json.Unmarshaller interface for type ComponentsKhmx01SchemasGenericresourceAllof0.
+func (c *ComponentsKhmx01SchemasGenericresourceAllof0) UnmarshalJSON(data []byte) error {
+ var rawMsg map[string]json.RawMessage
+ if err := json.Unmarshal(data, &rawMsg); err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", c, err)
+ }
+ for key, val := range rawMsg {
+ var err error
+ switch key {
+ case "id":
+ err = unpopulate(val, "ID", &c.ID)
+ delete(rawMsg, key)
+ case "name":
+ err = unpopulate(val, "Name", &c.Name)
+ delete(rawMsg, key)
+ case "properties":
+ err = unpopulate(val, "Properties", &c.Properties)
+ delete(rawMsg, key)
+ case "systemData":
+ err = unpopulate(val, "SystemData", &c.SystemData)
+ delete(rawMsg, key)
+ case "type":
+ err = unpopulate(val, "Type", &c.Type)
+ delete(rawMsg, key)
+ }
+ if err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", c, err)
+ }
+ }
+ return nil
+}
+
// MarshalJSON implements the json.Marshaller interface for type CredentialStorageProperties.
func (c CredentialStorageProperties) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]any)
@@ -512,6 +555,80 @@ func (e *ErrorResponse) UnmarshalJSON(data []byte) error {
return nil
}
+// MarshalJSON implements the json.Marshaller interface for type GenericResource.
+func (g GenericResource) MarshalJSON() ([]byte, error) {
+ objectMap := make(map[string]any)
+ populate(objectMap, "id", g.ID)
+ populate(objectMap, "name", g.Name)
+ populate(objectMap, "properties", g.Properties)
+ populate(objectMap, "systemData", g.SystemData)
+ populate(objectMap, "type", g.Type)
+ return json.Marshal(objectMap)
+}
+
+// UnmarshalJSON implements the json.Unmarshaller interface for type GenericResource.
+func (g *GenericResource) UnmarshalJSON(data []byte) error {
+ var rawMsg map[string]json.RawMessage
+ if err := json.Unmarshal(data, &rawMsg); err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", g, err)
+ }
+ for key, val := range rawMsg {
+ var err error
+ switch key {
+ case "id":
+ err = unpopulate(val, "ID", &g.ID)
+ delete(rawMsg, key)
+ case "name":
+ err = unpopulate(val, "Name", &g.Name)
+ delete(rawMsg, key)
+ case "properties":
+ err = unpopulate(val, "Properties", &g.Properties)
+ delete(rawMsg, key)
+ case "systemData":
+ err = unpopulate(val, "SystemData", &g.SystemData)
+ delete(rawMsg, key)
+ case "type":
+ err = unpopulate(val, "Type", &g.Type)
+ delete(rawMsg, key)
+ }
+ if err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", g, err)
+ }
+ }
+ return nil
+}
+
+// MarshalJSON implements the json.Marshaller interface for type GenericResourceListResult.
+func (g GenericResourceListResult) MarshalJSON() ([]byte, error) {
+ objectMap := make(map[string]any)
+ populate(objectMap, "nextLink", g.NextLink)
+ populate(objectMap, "value", g.Value)
+ return json.Marshal(objectMap)
+}
+
+// UnmarshalJSON implements the json.Unmarshaller interface for type GenericResourceListResult.
+func (g *GenericResourceListResult) UnmarshalJSON(data []byte) error {
+ var rawMsg map[string]json.RawMessage
+ if err := json.Unmarshal(data, &rawMsg); err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", g, err)
+ }
+ for key, val := range rawMsg {
+ var err error
+ switch key {
+ case "nextLink":
+ err = unpopulate(val, "NextLink", &g.NextLink)
+ delete(rawMsg, key)
+ case "value":
+ err = unpopulate(val, "Value", &g.Value)
+ delete(rawMsg, key)
+ }
+ if err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", g, err)
+ }
+ }
+ return nil
+}
+
// MarshalJSON implements the json.Marshaller interface for type InternalCredentialStorageProperties.
func (i InternalCredentialStorageProperties) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]any)
@@ -691,6 +808,45 @@ func (p *PlaneResourceTagsUpdate) UnmarshalJSON(data []byte) error {
return nil
}
+// MarshalJSON implements the json.Marshaller interface for type ProxyResource.
+func (p ProxyResource) MarshalJSON() ([]byte, error) {
+ objectMap := make(map[string]any)
+ populate(objectMap, "id", p.ID)
+ populate(objectMap, "name", p.Name)
+ populate(objectMap, "systemData", p.SystemData)
+ populate(objectMap, "type", p.Type)
+ return json.Marshal(objectMap)
+}
+
+// UnmarshalJSON implements the json.Unmarshaller interface for type ProxyResource.
+func (p *ProxyResource) UnmarshalJSON(data []byte) error {
+ var rawMsg map[string]json.RawMessage
+ if err := json.Unmarshal(data, &rawMsg); err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", p, err)
+ }
+ for key, val := range rawMsg {
+ var err error
+ switch key {
+ case "id":
+ err = unpopulate(val, "ID", &p.ID)
+ delete(rawMsg, key)
+ case "name":
+ err = unpopulate(val, "Name", &p.Name)
+ delete(rawMsg, key)
+ case "systemData":
+ err = unpopulate(val, "SystemData", &p.SystemData)
+ delete(rawMsg, key)
+ case "type":
+ err = unpopulate(val, "Type", &p.Type)
+ delete(rawMsg, key)
+ }
+ if err != nil {
+ return fmt.Errorf("unmarshalling type %T: %v", p, err)
+ }
+ }
+ return nil
+}
+
// MarshalJSON implements the json.Marshaller interface for type Resource.
func (r Resource) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]any)
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_options.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_options.go
index 4803307634..92d7c3ec05 100644
--- a/pkg/ucp/api/v20220901privatepreview/zz_generated_options.go
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_options.go
@@ -118,3 +118,8 @@ type ResourceGroupsClientUpdateOptions struct {
// placeholder for future optional parameters
}
+// ResourcesClientListOptions contains the optional parameters for the ResourcesClient.NewListPager method.
+type ResourcesClientListOptions struct {
+ // placeholder for future optional parameters
+}
+
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_resources_client.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_resources_client.go
new file mode 100644
index 0000000000..2b54a983c2
--- /dev/null
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_resources_client.go
@@ -0,0 +1,108 @@
+//go:build go1.18
+// +build go1.18
+
+// Licensed under the Apache License, Version 2.0 . See LICENSE in the repository root for license information.
+// Code generated by Microsoft (R) AutoRest Code Generator. DO NOT EDIT.
+// Changes may cause incorrect behavior and will be lost if the code is regenerated.
+
+package v20220901privatepreview
+
+import (
+ "context"
+ "errors"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// ResourcesClient contains the methods for the Resources group.
+// Don't use this type directly, use NewResourcesClient() instead.
+type ResourcesClient struct {
+ internal *arm.Client
+}
+
+// NewResourcesClient creates a new instance of ResourcesClient with the specified values.
+// - credential - used to authorize requests. Usually a credential from azidentity.
+// - options - pass nil to accept the default values.
+func NewResourcesClient(credential azcore.TokenCredential, options *arm.ClientOptions) (*ResourcesClient, error) {
+ cl, err := arm.NewClient(moduleName+".ResourcesClient", moduleVersion, credential, options)
+ if err != nil {
+ return nil, err
+ }
+ client := &ResourcesClient{
+ internal: cl,
+ }
+ return client, nil
+}
+
+// NewListPager - List resources in a resource group
+//
+// Generated from API version 2022-09-01-privatepreview
+// - planeType - The plane type.
+// - planeName - The name of the plane
+// - resourceGroupName - The name of resource group
+// - options - ResourcesClientListOptions contains the optional parameters for the ResourcesClient.NewListPager method.
+func (client *ResourcesClient) NewListPager(planeType string, planeName string, resourceGroupName string, options *ResourcesClientListOptions) (*runtime.Pager[ResourcesClientListResponse]) {
+ return runtime.NewPager(runtime.PagingHandler[ResourcesClientListResponse]{
+ More: func(page ResourcesClientListResponse) bool {
+ return page.NextLink != nil && len(*page.NextLink) > 0
+ },
+ Fetcher: func(ctx context.Context, page *ResourcesClientListResponse) (ResourcesClientListResponse, error) {
+ var req *policy.Request
+ var err error
+ if page == nil {
+ req, err = client.listCreateRequest(ctx, planeType, planeName, resourceGroupName, options)
+ } else {
+ req, err = runtime.NewRequest(ctx, http.MethodGet, *page.NextLink)
+ }
+ if err != nil {
+ return ResourcesClientListResponse{}, err
+ }
+ resp, err := client.internal.Pipeline().Do(req)
+ if err != nil {
+ return ResourcesClientListResponse{}, err
+ }
+ if !runtime.HasStatusCode(resp, http.StatusOK) {
+ return ResourcesClientListResponse{}, runtime.NewResponseError(resp)
+ }
+ return client.listHandleResponse(resp)
+ },
+ })
+}
+
+// listCreateRequest creates the List request.
+func (client *ResourcesClient) listCreateRequest(ctx context.Context, planeType string, planeName string, resourceGroupName string, options *ResourcesClientListOptions) (*policy.Request, error) {
+ urlPath := "/planes/{planeType}/{planeName}/resourcegroups/{resourceGroupName}/resources"
+ if planeType == "" {
+ return nil, errors.New("parameter planeType cannot be empty")
+ }
+ urlPath = strings.ReplaceAll(urlPath, "{planeType}", url.PathEscape(planeType))
+ urlPath = strings.ReplaceAll(urlPath, "{planeName}", planeName)
+ if resourceGroupName == "" {
+ return nil, errors.New("parameter resourceGroupName cannot be empty")
+ }
+ urlPath = strings.ReplaceAll(urlPath, "{resourceGroupName}", url.PathEscape(resourceGroupName))
+ req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.internal.Endpoint(), urlPath))
+ if err != nil {
+ return nil, err
+ }
+ reqQP := req.Raw().URL.Query()
+ reqQP.Set("api-version", "2022-09-01-privatepreview")
+ req.Raw().URL.RawQuery = reqQP.Encode()
+ req.Raw().Header["Accept"] = []string{"application/json"}
+ return req, nil
+}
+
+// listHandleResponse handles the List response.
+func (client *ResourcesClient) listHandleResponse(resp *http.Response) (ResourcesClientListResponse, error) {
+ result := ResourcesClientListResponse{}
+ if err := runtime.UnmarshalAsJSON(resp, &result.GenericResourceListResult); err != nil {
+ return ResourcesClientListResponse{}, err
+ }
+ return result, nil
+}
+
diff --git a/pkg/ucp/api/v20220901privatepreview/zz_generated_response_types.go b/pkg/ucp/api/v20220901privatepreview/zz_generated_response_types.go
index 8ff262c9ad..f3990338b0 100644
--- a/pkg/ucp/api/v20220901privatepreview/zz_generated_response_types.go
+++ b/pkg/ucp/api/v20220901privatepreview/zz_generated_response_types.go
@@ -129,3 +129,9 @@ type ResourceGroupsClientUpdateResponse struct {
ResourceGroupResource
}
+// ResourcesClientListResponse contains the response from method ResourcesClient.NewListPager.
+type ResourcesClientListResponse struct {
+ // The response of a GenericResource list operation.
+ GenericResourceListResult
+}
+
diff --git a/pkg/ucp/datamodel/converter/genericresource_converter.go b/pkg/ucp/datamodel/converter/genericresource_converter.go
new file mode 100644
index 0000000000..7d2cb4bbc1
--- /dev/null
+++ b/pkg/ucp/datamodel/converter/genericresource_converter.go
@@ -0,0 +1,47 @@
+/*
+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 converter
+
+import (
+ "errors"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ v20220901privatepreview "github.com/radius-project/radius/pkg/ucp/api/v20220901privatepreview"
+ "github.com/radius-project/radius/pkg/ucp/datamodel"
+)
+
+// GenericResourceDataModelToVersioned converts version agnostic datamodel to versioned model.
+// It returns an error if the conversion fails.
+func GenericResourceDataModelToVersioned(model *datamodel.GenericResource, version string) (v1.VersionedModelInterface, error) {
+ switch version {
+ case v20220901privatepreview.Version:
+ versioned := &v20220901privatepreview.GenericResource{}
+ if err := versioned.ConvertFrom(model); err != nil {
+ return nil, err
+ }
+ return versioned, nil
+
+ default:
+ return nil, v1.ErrUnsupportedAPIVersion
+ }
+}
+
+// GenericResourceDataModelFromVersioned converts versioned model to datamodel.
+// It returns an error if the conversion fails.
+func GenericResourceDataModelFromVersioned(content []byte, version string) (*datamodel.GenericResource, error) {
+ return nil, errors.New("the GenericResource type does not support conversion from versioned models")
+}
diff --git a/pkg/ucp/datamodel/genericresource.go b/pkg/ucp/datamodel/genericresource.go
new file mode 100644
index 0000000000..f80d60a3b2
--- /dev/null
+++ b/pkg/ucp/datamodel/genericresource.go
@@ -0,0 +1,55 @@
+/*
+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 datamodel
+
+import v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+
+// GenericResource represents a stored "tracked resource" within a UCP resource group.
+//
+// This type is used to store tracked resources within UCP regardless of the actual
+// resource type. You can think of it as a "meta-resource". The top level fields like "ID",
+// "Name", and "Type" reflect the GenericResource entry itself. The actual resource data
+// is stored in the "Properties" field.
+//
+// GenericResource are returned through the resource list APIs, but don't support PUT or
+// DELETE operations directly. The resource ID, Name, and Type of the GenericResource
+// are an implementation detail and are never exposed to users.
+type GenericResource struct {
+ v1.BaseResource
+
+ // Properties stores the properties of the resource being tracked.
+ Properties GenericResourceProperties `json:"properties"`
+}
+
+// ResourceTypeName gives the type of ucp resource.
+func (r *GenericResource) ResourceTypeName() string {
+ return "System.Resources/resources"
+}
+
+// GenericResourceProperties stores the properties of the resource being tracked.
+//
+// Right now we only track the basic identifiers. This is enough for UCP to remebmer
+// which resources exist, but not to act as a cache. We may want to add more fields
+// in the future as we support additional scenarios.
+type GenericResourceProperties struct {
+ // ID is the fully qualified resource ID for the resource.
+ ID string `json:"id"`
+ // Name is the resource name.
+ Name string `json:"name"`
+ // Type is the resource type.
+ Type string `json:"type"`
+}
diff --git a/pkg/ucp/datamodel/resource.go b/pkg/ucp/datamodel/resource.go
deleted file mode 100644
index edcda5fd67..0000000000
--- a/pkg/ucp/datamodel/resource.go
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-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 datamodel
-
-// Resource represents a resource within a UCP resource group
-type Resource struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
-}
-
-// ResourceList represents a list of resources
-type ResourceList struct {
- Value []Resource `json:"value" yaml:"value"`
-}
diff --git a/pkg/ucp/frontend/controller/resourcegroups/listresources.go b/pkg/ucp/frontend/controller/resourcegroups/listresources.go
new file mode 100644
index 0000000000..c0d5d35b4f
--- /dev/null
+++ b/pkg/ucp/frontend/controller/resourcegroups/listresources.go
@@ -0,0 +1,110 @@
+/*
+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 resourcegroups
+
+import (
+ "context"
+ "errors"
+ http "net/http"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ armrpc_controller "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ armrpc_rest "github.com/radius-project/radius/pkg/armrpc/rest"
+ "github.com/radius-project/radius/pkg/middleware"
+ "github.com/radius-project/radius/pkg/ucp/api/v20220901privatepreview"
+ "github.com/radius-project/radius/pkg/ucp/datamodel"
+ "github.com/radius-project/radius/pkg/ucp/datamodel/converter"
+ "github.com/radius-project/radius/pkg/ucp/resources"
+ "github.com/radius-project/radius/pkg/ucp/store"
+)
+
+var _ armrpc_controller.Controller = (*ListResources)(nil)
+
+// ListResources is the controller implementation to get the list of resources stored in a resource group.
+type ListResources struct {
+ armrpc_controller.Operation[*datamodel.GenericResource, datamodel.GenericResource]
+}
+
+// NewListResources creates a new controller for listing resources stored in a resource group.
+func NewListResources(opts armrpc_controller.Options) (armrpc_controller.Controller, error) {
+ return &ListResources{
+ Operation: armrpc_controller.NewOperation(opts,
+ armrpc_controller.ResourceOptions[datamodel.GenericResource]{
+ RequestConverter: converter.GenericResourceDataModelFromVersioned,
+ ResponseConverter: converter.GenericResourceDataModelToVersioned,
+ },
+ ),
+ }, nil
+}
+
+// Run implements controller.Controller.
+func (r *ListResources) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (armrpc_rest.Response, error) {
+ relativePath := middleware.GetRelativePath(r.Options().PathBase, req.URL.Path)
+ id, err := resources.Parse(relativePath)
+ if err != nil {
+ return nil, err
+ }
+
+ // Cut off the "resources" part of the ID. The ID should be the ID of a resource group.
+ resourceGroupID := id.Truncate()
+
+ // First check if the resource group exists.
+ _, err = r.StorageClient().Get(ctx, resourceGroupID.String())
+ if errors.Is(err, &store.ErrNotFound{}) {
+ return armrpc_rest.NewNotFoundResponse(id), nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ query := store.Query{
+ RootScope: resourceGroupID.String(),
+ ResourceType: v20220901privatepreview.ResourceType,
+ }
+
+ result, err := r.StorageClient().Query(ctx, query)
+ if err != nil {
+ return nil, err
+ }
+
+ response, err := r.createResponse(ctx, req, result)
+ if err != nil {
+ return nil, err
+ }
+
+ return armrpc_rest.NewOKResponse(response), nil
+}
+
+func (r *ListResources) createResponse(ctx context.Context, req *http.Request, result *store.ObjectQueryResult) (*v1.PaginatedList, error) {
+ items := v1.PaginatedList{}
+ serviceCtx := v1.ARMRequestContextFromContext(ctx)
+
+ for _, item := range result.Items {
+ data := datamodel.GenericResource{}
+ err := item.As(&data)
+ if err != nil {
+ return nil, err
+ }
+
+ versioned, err := converter.GenericResourceDataModelToVersioned(&data, serviceCtx.APIVersion)
+ if err != nil {
+ return nil, err
+ }
+
+ items.Value = append(items.Value, versioned)
+ }
+
+ return &items, nil
+}
diff --git a/pkg/ucp/frontend/controller/resourcegroups/listresources_test.go b/pkg/ucp/frontend/controller/resourcegroups/listresources_test.go
new file mode 100644
index 0000000000..3a933d372f
--- /dev/null
+++ b/pkg/ucp/frontend/controller/resourcegroups/listresources_test.go
@@ -0,0 +1,144 @@
+/*
+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 resourcegroups
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ armrpc_controller "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ armrpc_rest "github.com/radius-project/radius/pkg/armrpc/rest"
+ "github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/radius-project/radius/pkg/to"
+ "github.com/radius-project/radius/pkg/ucp/api/v20220901privatepreview"
+ "github.com/radius-project/radius/pkg/ucp/datamodel"
+ "github.com/radius-project/radius/pkg/ucp/resources"
+ "github.com/radius-project/radius/pkg/ucp/store"
+)
+
+func Test_ListResources(t *testing.T) {
+ entryResource := v20220901privatepreview.GenericResource{
+ ID: to.Ptr("/planes/radius/local/resourceGroups/test-rg/providers/Applications.Core/applications/test-app"),
+ Type: to.Ptr("Applications.Core/applications"),
+ Name: to.Ptr("test-app"),
+ }
+ entryDatamodel := datamodel.GenericResource{
+ BaseResource: v1.BaseResource{
+ TrackedResource: v1.TrackedResource{
+ ID: "ignored",
+ Type: "ignored",
+ Name: "ignored",
+ },
+ },
+ Properties: datamodel.GenericResourceProperties{
+ ID: *entryResource.ID,
+ Type: *entryResource.Type,
+ Name: *entryResource.Name,
+ },
+ }
+
+ // Not currently used, but may be in the future.
+ resourceGroupDatamodel := datamodel.ResourceGroup{}
+
+ resourceGroupID := "/planes/radius/local/resourceGroups/test-rg"
+ id := resourceGroupID + "/resources"
+
+ t.Run("success", func(t *testing.T) {
+ storage, ctrl := setupListResources(t)
+
+ storage.EXPECT().
+ Get(gomock.Any(), resourceGroupID).
+ Return(&store.Object{Data: resourceGroupDatamodel}, nil).
+ Times(1)
+
+ expectedQuery := store.Query{RootScope: resourceGroupID, ResourceType: v20220901privatepreview.ResourceType}
+ storage.EXPECT().
+ Query(gomock.Any(), expectedQuery).
+ Return(&store.ObjectQueryResult{Items: []store.Object{{Data: entryDatamodel}}}, nil).
+ Times(1)
+
+ expected := armrpc_rest.NewOKResponse(&v1.PaginatedList{
+ Value: []any{&entryResource},
+ })
+
+ request, err := http.NewRequest(http.MethodGet, ctrl.Options().PathBase+id+"?api-version="+v20220901privatepreview.Version, nil)
+ require.NoError(t, err)
+ ctx := rpctest.NewARMRequestContext(request)
+ response, err := ctrl.Run(ctx, nil, request)
+ require.NoError(t, err)
+ require.Equal(t, expected, response)
+ })
+
+ t.Run("success - empty", func(t *testing.T) {
+ storage, ctrl := setupListResources(t)
+
+ storage.EXPECT().
+ Get(gomock.Any(), resourceGroupID).
+ Return(&store.Object{Data: resourceGroupDatamodel}, nil).
+ Times(1)
+
+ expectedQuery := store.Query{RootScope: resourceGroupID, ResourceType: v20220901privatepreview.ResourceType}
+ storage.EXPECT().
+ Query(gomock.Any(), expectedQuery).
+ Return(&store.ObjectQueryResult{Items: []store.Object{}}, nil).
+ Times(1)
+
+ expected := armrpc_rest.NewOKResponse(&v1.PaginatedList{})
+
+ request, err := http.NewRequest(http.MethodGet, ctrl.Options().PathBase+id+"?api-version="+v20220901privatepreview.Version, nil)
+ require.NoError(t, err)
+ ctx := rpctest.NewARMRequestContext(request)
+ response, err := ctrl.Run(ctx, nil, request)
+ require.NoError(t, err)
+ require.Equal(t, expected, response)
+ })
+
+ t.Run("resource group not found", func(t *testing.T) {
+ storage, ctrl := setupListResources(t)
+
+ storage.EXPECT().
+ Get(gomock.Any(), resourceGroupID).
+ Return(nil, &store.ErrNotFound{ID: resourceGroupID}).
+ Times(1)
+
+ parsed, err := resources.Parse(id)
+ require.NoError(t, err)
+
+ expected := armrpc_rest.NewNotFoundResponse(parsed)
+
+ request, err := http.NewRequest(http.MethodGet, ctrl.Options().PathBase+id+"?api-version="+v20220901privatepreview.Version, nil)
+ require.NoError(t, err)
+ ctx := rpctest.NewARMRequestContext(request)
+ response, err := ctrl.Run(ctx, nil, request)
+ require.NoError(t, err)
+ require.Equal(t, expected, response)
+ })
+}
+
+func setupListResources(t *testing.T) (*store.MockStorageClient, *ListResources) {
+ ctrl := gomock.NewController(t)
+ storage := store.NewMockStorageClient(ctrl)
+
+ c, err := NewListResources(armrpc_controller.Options{StorageClient: storage, PathBase: "/" + uuid.New().String()})
+ require.NoError(t, err)
+
+ return storage, c.(*ListResources)
+}
diff --git a/pkg/ucp/frontend/radius/routes.go b/pkg/ucp/frontend/radius/routes.go
index 1c5a17be29..294e12eb9e 100644
--- a/pkg/ucp/frontend/radius/routes.go
+++ b/pkg/ucp/frontend/radius/routes.go
@@ -99,19 +99,28 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) {
)
},
},
+ {
+ ParentRouter: resourceGroupResourceRouter,
+ ResourceType: v20220901privatepreview.ResourceType,
+ Path: "/resources",
+ Method: v1.OperationList,
+ ControllerFactory: func(opt controller.Options) (controller.Controller, error) {
+ return resourcegroups_ctrl.NewListResources(opt)
+ },
+ },
// Chi router uses radix tree so that it doesn't linear search the matched one. So, to catch all requests,
// we need to use CatchAllPath(/*) at the above matched routes path in chi router.
//
// Note that the API validation is not applied for CatchAllPath(/*).
{
- // Proxy request should use CatchAllPath(/*) to process all requests under /planes/azure/{planeName}/resourcegroups/{resourceGroupName}.
+ // Proxy request should use CatchAllPath(/*) to process all requests under /planes/radius/{planeName}/resourcegroups/{resourceGroupName}.
ParentRouter: resourceGroupResourceRouter,
Path: server.CatchAllPath,
OperationType: &v1.OperationType{Type: OperationTypeUCPRadiusProxy, Method: v1.OperationProxy},
ControllerFactory: planes_ctrl.NewProxyController,
},
{
- // Proxy request should use CatchAllPath(/*) to process all requests under /planes/azure/{planeName}/.
+ // Proxy request should use CatchAllPath(/*) to process all requests under /planes/radius/{planeName}/.
ParentRouter: baseRouter,
Path: server.CatchAllPath,
OperationType: &v1.OperationType{Type: OperationTypeUCPRadiusProxy, Method: v1.OperationProxy},
diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Resources_List.json b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Resources_List.json
new file mode 100644
index 0000000000..47836a988a
--- /dev/null
+++ b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Resources_List.json
@@ -0,0 +1,30 @@
+{
+ "operationId": "Resources_List",
+ "title": "List resources in a resource group.",
+ "parameters": {
+ "api-version": "2022-09-01-privatepreview",
+ "planeName": "local",
+ "planeType": "radius",
+ "resourceGroupName": "rg1"
+ },
+ "responses": {
+ "200": {
+ "body": {
+ "value": [
+ {
+ "id": "/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/containers/my-container",
+ "name": "my-container",
+ "location": "global",
+ "type": "Applications.Core/containers"
+ },
+ {
+ "id": "/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/applications/my-application",
+ "name": "my-application",
+ "location": "global",
+ "type": "Applications.Core/applications"
+ }
+ ]
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/openapi.json b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/openapi.json
index 4960817d11..a1a88fa46c 100644
--- a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/openapi.json
+++ b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/openapi.json
@@ -45,6 +45,9 @@
{
"name": "ResourceGroups"
},
+ {
+ "name": "Resources"
+ },
{
"name": "AwsCredentials"
},
@@ -641,6 +644,61 @@
}
}
},
+ "/planes/{planeType}/{planeName}/resourcegroups/{resourceGroupName}/resources": {
+ "get": {
+ "operationId": "Resources_List",
+ "tags": [
+ "Resources"
+ ],
+ "description": "List resources in a resource group",
+ "parameters": [
+ {
+ "$ref": "../../../../../common-types/resource-management/v3/types.json#/parameters/ApiVersionParameter"
+ },
+ {
+ "name": "planeType",
+ "in": "path",
+ "description": "The plane type.",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "$ref": "#/parameters/PlaneNameParameter"
+ },
+ {
+ "name": "resourceGroupName",
+ "in": "path",
+ "description": "The name of resource group",
+ "required": true,
+ "type": "string",
+ "maxLength": 63,
+ "pattern": "^[A-Za-z]([-A-Za-z0-9]*[A-Za-z0-9])?$"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "ARM operation completed successfully.",
+ "schema": {
+ "$ref": "#/definitions/GenericResourceListResult"
+ }
+ },
+ "default": {
+ "description": "An unexpected error response.",
+ "schema": {
+ "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ErrorResponse"
+ }
+ }
+ },
+ "x-ms-examples": {
+ "List resources in a resource group.": {
+ "$ref": "./examples/Resources_List.json"
+ }
+ },
+ "x-ms-pageable": {
+ "nextLinkName": "nextLink"
+ }
+ }
+ },
"/planes/aws/{planeName}/providers/System.AWS/credentials": {
"get": {
"operationId": "AwsCredentials_List",
@@ -1399,6 +1457,63 @@
"kind"
]
},
+ "GenericResource": {
+ "type": "object",
+ "description": "Represents resource data.",
+ "properties": {
+ "name": {
+ "$ref": "#/definitions/ResourceNameString",
+ "description": "The name of resource",
+ "readOnly": true
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "allOf": [
+ {
+ "type": "object",
+ "description": "Concrete proxy resource types can be created by aliasing this type using a specific property type.",
+ "properties": {
+ "properties": {
+ "$ref": "#/definitions/ResourceProperties",
+ "description": "The resource-specific properties for this resource.",
+ "x-ms-client-flatten": true,
+ "x-ms-mutability": [
+ "read",
+ "create"
+ ]
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "../../../../../common-types/resource-management/v3/types.json#/definitions/ProxyResource"
+ }
+ ]
+ }
+ ]
+ },
+ "GenericResourceListResult": {
+ "type": "object",
+ "description": "The response of a GenericResource list operation.",
+ "properties": {
+ "value": {
+ "type": "array",
+ "description": "The GenericResource items on this page",
+ "items": {
+ "$ref": "#/definitions/GenericResource"
+ }
+ },
+ "nextLink": {
+ "type": "string",
+ "format": "uri",
+ "description": "The link to the next page of items"
+ }
+ },
+ "required": [
+ "value"
+ ]
+ },
"InternalCredentialStorageProperties": {
"type": "object",
"description": "Internal credential storage properties",
@@ -1652,6 +1767,17 @@
}
}
},
+ "ResourceNameString": {
+ "type": "string",
+ "description": "The resource name",
+ "maxLength": 63,
+ "pattern": "^[A-Za-z]([-A-Za-z0-9]*[A-Za-z0-9])?$"
+ },
+ "ResourceProperties": {
+ "type": "object",
+ "description": "The resource properties",
+ "properties": {}
+ },
"Versions": {
"type": "string",
"description": "Supported API versions for Universal Control Plane resource provider.",
diff --git a/typespec/UCP/examples/2022-09-01-privatepreview/Resources_List.json b/typespec/UCP/examples/2022-09-01-privatepreview/Resources_List.json
new file mode 100644
index 0000000000..47836a988a
--- /dev/null
+++ b/typespec/UCP/examples/2022-09-01-privatepreview/Resources_List.json
@@ -0,0 +1,30 @@
+{
+ "operationId": "Resources_List",
+ "title": "List resources in a resource group.",
+ "parameters": {
+ "api-version": "2022-09-01-privatepreview",
+ "planeName": "local",
+ "planeType": "radius",
+ "resourceGroupName": "rg1"
+ },
+ "responses": {
+ "200": {
+ "body": {
+ "value": [
+ {
+ "id": "/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/containers/my-container",
+ "name": "my-container",
+ "location": "global",
+ "type": "Applications.Core/containers"
+ },
+ {
+ "id": "/planes/radius/local/resourcegroups/rg1/providers/Applications.Core/applications/my-application",
+ "name": "my-application",
+ "location": "global",
+ "type": "Applications.Core/applications"
+ }
+ ]
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/typespec/UCP/resourcegroups.tsp b/typespec/UCP/resourcegroups.tsp
index db83f4c466..03edb5c601 100644
--- a/typespec/UCP/resourcegroups.tsp
+++ b/typespec/UCP/resourcegroups.tsp
@@ -53,6 +53,20 @@ model ResourceGroupProperties {
provisioningState?: ProvisioningState;
}
+@doc("Represents resource data.")
+@parentResource(ResourceGroupResource)
+model GenericResource extends ProxyResource {
+ @doc("The name of resource")
+ @path
+ @key("resourceName")
+ @segment("resources")
+ @visibility("read")
+ name: ResourceNameString;
+}
+
+@doc("The resource properties")
+model ResourceProperties {}
+
@doc("The UCP HTTP request base parameters.")
model ResourceGroupBaseParameters {
...PlaneBaseParameters;
@@ -91,3 +105,9 @@ interface ResourceGroups {
ResourceGroupBaseParameters
>;
}
+
+@armResourceOperations
+interface Resources {
+ @doc("List resources in a resource group")
+ list is UcpResourceList>;
+}
From b48e549f96b8647b26323518b276fc2bf9141313 Mon Sep 17 00:00:00 2001
From: Yetkin Timocin
Date: Wed, 6 Sep 2023 14:10:51 -0700
Subject: [PATCH 04/13] Adding applyDeploymentOutput to the necessary resources
(#6203)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Adding applyDeploymentOutput to the necessary resources but not sure if
that function (applyDeploymentOutput) is longer being used for portable
resources (after the Processor implementation).
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #5396
## Auto-generated summary
### ๐ค Generated by Copilot at 851e1f2
### Summary
๐๐๐
This pull request adds support for the Dapr secret store feature and
improves the Dapr component creation process. It updates the
DaprPubSubBroker and DaprSecretStore resources to use the renderers
package and store the computed values from the DeploymentOutput object.
It also updates the LocalID constants to match the current resources and
adds unit tests for the Dapr resources.
> _We're sailing on the Dapr seas, with secrets and pubsub_
> _We're rendering and testing, to make our code more robust_
> _We're heaving on the yardarm, on the count of three_
> _We're updating our `localids`, to match our resources_
### Walkthrough
* Add and assign ComputedValues and SecretValues fields to SecretStore,
DaprPubSubBroker, and DaprSecretStore resources from DeploymentOutput
object
([link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-9f935e3c9932333a6c9f098ec1e56cf83d72bc99478430a73c839b44288a8eb4R69-R70),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-a74b5d21cf74e51c5ab93a6f4634a606a4f869aa1b9a279fb25df2fdcc386d2dL39-R48),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-e58a6737964c53219141f95631bcc9c8e7bb532d131b03919fd9593cf29b6dbaR41-R45))
* Set ComponentName property of DaprPubSubBroker and DaprSecretStore
resources based on computed value from DeploymentOutput object
([link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-a74b5d21cf74e51c5ab93a6f4634a606a4f869aa1b9a279fb25df2fdcc386d2dL39-R48),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-e58a6737964c53219141f95631bcc9c8e7bb532d131b03919fd9593cf29b6dbaR41-R45))
* Import renderers package to use ComponentNameKey constant in
`daprpubsubbroker.go` and `daprsecretstore.go` files
([link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-a74b5d21cf74e51c5ab93a6f4634a606a4f869aa1b9a279fb25df2fdcc386d2dR23),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-e58a6737964c53219141f95631bcc9c8e7bb532d131b03919fd9593cf29b6dbaR23))
* Add unit tests for ApplyDeploymentOutput method and fields of
DaprPubSubBroker and DaprSecretStore resources in
`daprpubsubbroker_test.go` and `daprsecretstore_test.go` files
([link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-ef7ee8ae2ed1365eff1acc39de71a720b8819ddc6a40686bd2b30ce4f5e183cfR1-R85),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-660e457f6d85e692ba69a80bed51332131453001ac9098fe751761dbd82dda4bR1-R85))
* Update list of LocalID constants in `localids.go` file to remove
unused or obsolete ones and add new ones for Dapr resources
([link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-1a8ed1597b7f87b2cb720a92c0e95326b83c0c22c8afc96a9bb4dcac66eb37f9L50-R57),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-1a8ed1597b7f87b2cb720a92c0e95326b83c0c22c8afc96a9bb4dcac66eb37f9L82),
[link](https://github.com/radius-project/radius/pull/6203/files?diff=unified&w=0#diff-1a8ed1597b7f87b2cb720a92c0e95326b83c0c22c8afc96a9bb4dcac66eb37f9L88-R70))
---
pkg/corerp/datamodel/secretstore.go | 2 +
pkg/daprrp/datamodel/daprpubsubbroker.go | 4 +-
pkg/daprrp/datamodel/daprsecretstore.go | 3 +-
pkg/daprrp/datamodel/daprstatestore.go | 7 --
pkg/daprrp/datamodel/daprstatestore_test.go | 85 -------------------
pkg/datastoresrp/datamodel/mongodatabase.go | 8 --
pkg/datastoresrp/datamodel/rediscache.go | 30 -------
pkg/datastoresrp/datamodel/sqldatabase.go | 1 -
pkg/messagingrp/datamodel/rabbitmq.go | 1 -
.../controller/createorupdateresource_test.go | 1 -
pkg/rp/v1/localids.go | 26 +-----
11 files changed, 7 insertions(+), 161 deletions(-)
delete mode 100644 pkg/daprrp/datamodel/daprstatestore_test.go
diff --git a/pkg/corerp/datamodel/secretstore.go b/pkg/corerp/datamodel/secretstore.go
index 5a3e7d24e0..d7cbd55af2 100644
--- a/pkg/corerp/datamodel/secretstore.go
+++ b/pkg/corerp/datamodel/secretstore.go
@@ -66,6 +66,8 @@ func (s *SecretStore) ResourceTypeName() string {
// object and returns no error.
func (s *SecretStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
s.Properties.Status.OutputResources = do.DeployedOutputResources
+ s.ComputedValues = do.ComputedValues
+ s.SecretValues = do.SecretValues
return nil
}
diff --git a/pkg/daprrp/datamodel/daprpubsubbroker.go b/pkg/daprrp/datamodel/daprpubsubbroker.go
index e89499107b..94b2553b21 100644
--- a/pkg/daprrp/datamodel/daprpubsubbroker.go
+++ b/pkg/daprrp/datamodel/daprpubsubbroker.go
@@ -34,10 +34,8 @@ type DaprPubSubBroker struct {
pr_dm.LinkMetadata
}
-// ApplyDeploymentOutput applies the properties changes based on the deployment output. It updates the
-// OutputResources of the DaprPubSubBroker resource with the output resources from a DeploymentOutput object.
+// ApplyDeploymentOutput updates the DaprPubSubBroker resource with the DeploymentOutput values.
func (r *DaprPubSubBroker) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
return nil
}
diff --git a/pkg/daprrp/datamodel/daprsecretstore.go b/pkg/daprrp/datamodel/daprsecretstore.go
index fb9dc1fd15..41bfd2fc51 100644
--- a/pkg/daprrp/datamodel/daprsecretstore.go
+++ b/pkg/daprrp/datamodel/daprsecretstore.go
@@ -34,9 +34,8 @@ type DaprSecretStore struct {
pr_dm.LinkMetadata
}
-// ApplyDeploymentOutput updates the status of the secret store with the output resources from a deployment.
+// ApplyDeploymentOutput updates the DaprSecretStore resource with the DeploymentOutput values.
func (r *DaprSecretStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
return nil
}
diff --git a/pkg/daprrp/datamodel/daprstatestore.go b/pkg/daprrp/datamodel/daprstatestore.go
index ae224edb0e..6c2efc5869 100644
--- a/pkg/daprrp/datamodel/daprstatestore.go
+++ b/pkg/daprrp/datamodel/daprstatestore.go
@@ -20,7 +20,6 @@ import (
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/portableresources"
pr_dm "github.com/radius-project/radius/pkg/portableresources/datamodel"
- "github.com/radius-project/radius/pkg/portableresources/renderers"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
@@ -37,12 +36,6 @@ type DaprStateStore struct {
// ApplyDeploymentOutput updates the DaprStateStore resource with the DeploymentOutput values.
func (r *DaprStateStore) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
- r.ComputedValues = do.ComputedValues
- r.SecretValues = do.SecretValues
- if cn, ok := do.ComputedValues[renderers.ComponentNameKey].(string); ok {
- r.Properties.ComponentName = cn
- }
return nil
}
diff --git a/pkg/daprrp/datamodel/daprstatestore_test.go b/pkg/daprrp/datamodel/daprstatestore_test.go
deleted file mode 100644
index d376817fdb..0000000000
--- a/pkg/daprrp/datamodel/daprstatestore_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-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 datamodel
-
-import (
- "testing"
-
- "github.com/radius-project/radius/pkg/portableresources/renderers"
- rpv1 "github.com/radius-project/radius/pkg/rp/v1"
- "github.com/radius-project/radius/pkg/to"
- "github.com/stretchr/testify/require"
-)
-
-func TestDaprStateStore_ApplyDeploymentOutput(t *testing.T) {
- tests := []struct {
- name string
- dss *DaprStateStore
- do *rpv1.DeploymentOutput
- wantErr bool
- }{
- {
- name: "with component name",
- dss: &DaprStateStore{},
- do: &rpv1.DeploymentOutput{
- DeployedOutputResources: []rpv1.OutputResource{
- {
- LocalID: rpv1.LocalIDDaprStateStoreAzureStorage,
- RadiusManaged: to.Ptr(true),
- },
- },
- ComputedValues: map[string]any{
- renderers.ComponentNameKey: "dapr-state-store-test",
- },
- },
- wantErr: false,
- },
- {
- name: "without component name",
- dss: &DaprStateStore{},
- do: &rpv1.DeploymentOutput{
- DeployedOutputResources: []rpv1.OutputResource{
- {
- LocalID: rpv1.LocalIDDaprStateStoreAzureStorage,
- RadiusManaged: to.Ptr(true),
- },
- },
- ComputedValues: map[string]any{},
- },
- wantErr: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if err := tt.dss.ApplyDeploymentOutput(*tt.do); (err != nil) != tt.wantErr {
- t.Errorf("DaprStateStore.ApplyDeploymentOutput() error = %v, wantErr %v", err, tt.wantErr)
- }
-
- if !tt.wantErr {
- require.EqualValues(t, tt.do.DeployedOutputResources, tt.dss.Properties.Status.OutputResources)
- require.EqualValues(t, tt.do.ComputedValues, tt.dss.ComputedValues)
- require.EqualValues(t, tt.do.SecretValues, tt.dss.SecretValues)
- require.Condition(t, func() bool {
- if tt.do.ComputedValues[renderers.ComponentNameKey] != nil {
- return tt.dss.Properties.ComponentName == tt.do.ComputedValues[renderers.ComponentNameKey]
- }
- return tt.dss.Properties.ComponentName == ""
- }, "component name should be equal")
- }
- })
- }
-}
diff --git a/pkg/datastoresrp/datamodel/mongodatabase.go b/pkg/datastoresrp/datamodel/mongodatabase.go
index ea526354f1..3882d0ab37 100644
--- a/pkg/datastoresrp/datamodel/mongodatabase.go
+++ b/pkg/datastoresrp/datamodel/mongodatabase.go
@@ -23,7 +23,6 @@ import (
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/portableresources"
pr_dm "github.com/radius-project/radius/pkg/portableresources/datamodel"
- "github.com/radius-project/radius/pkg/portableresources/renderers"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
@@ -103,13 +102,6 @@ func (mongodb *MongoDatabase) VerifyInputs() error {
// ApplyDeploymentOutput updates the Mongo database instance's database property, output resources, computed values
// and secret values with the given DeploymentOutput.
func (r *MongoDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
- r.ComputedValues = do.ComputedValues
- r.SecretValues = do.SecretValues
- if database, ok := do.ComputedValues[renderers.DatabaseNameValue].(string); ok {
- r.Properties.Database = database
- }
-
return nil
}
diff --git a/pkg/datastoresrp/datamodel/rediscache.go b/pkg/datastoresrp/datamodel/rediscache.go
index 4d1ce293b9..1b9db8f07e 100644
--- a/pkg/datastoresrp/datamodel/rediscache.go
+++ b/pkg/datastoresrp/datamodel/rediscache.go
@@ -17,15 +17,12 @@ limitations under the License.
package datamodel
import (
- "errors"
"fmt"
- "strconv"
"strings"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/portableresources"
pr_dm "github.com/radius-project/radius/pkg/portableresources/datamodel"
- "github.com/radius-project/radius/pkg/portableresources/renderers"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
@@ -43,33 +40,6 @@ type RedisCache struct {
// ApplyDeploymentOutput sets the Status, ComputedValues, SecretValues, Host, Port and Username properties of the
// Redis cache instance based on the DeploymentOutput object.
func (r *RedisCache) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
- r.ComputedValues = do.ComputedValues
- r.SecretValues = do.SecretValues
- if host, ok := do.ComputedValues[renderers.Host].(string); ok {
- r.Properties.Host = host
- }
- if port, ok := do.ComputedValues[renderers.Port]; ok {
- if port != nil {
- switch p := port.(type) {
- case float64:
- r.Properties.Port = int32(p)
- case int32:
- r.Properties.Port = p
- case string:
- converted, err := strconv.Atoi(p)
- if err != nil {
- return err
- }
- r.Properties.Port = int32(converted)
- default:
- return errors.New("unhandled type for the property port")
- }
- }
- }
- if username, ok := do.ComputedValues[renderers.UsernameStringValue].(string); ok {
- r.Properties.Username = username
- }
return nil
}
diff --git a/pkg/datastoresrp/datamodel/sqldatabase.go b/pkg/datastoresrp/datamodel/sqldatabase.go
index 2c67929475..1bf003ff86 100644
--- a/pkg/datastoresrp/datamodel/sqldatabase.go
+++ b/pkg/datastoresrp/datamodel/sqldatabase.go
@@ -49,7 +49,6 @@ type SqlDatabase struct {
// ApplyDeploymentOutput updates the output resources of a SQL database resource with the output resources of a DeploymentOutput
// object and returns no error.
func (r *SqlDatabase) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
return nil
}
diff --git a/pkg/messagingrp/datamodel/rabbitmq.go b/pkg/messagingrp/datamodel/rabbitmq.go
index 29a5aa28d5..a16b840a54 100644
--- a/pkg/messagingrp/datamodel/rabbitmq.go
+++ b/pkg/messagingrp/datamodel/rabbitmq.go
@@ -40,7 +40,6 @@ type RabbitMQQueue struct {
// ApplyDeploymentOutput updates the RabbitMQQueue instance with the DeployedOutputResources from the
// DeploymentOutput object and returns no error.
func (r *RabbitMQQueue) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
- r.Properties.Status.OutputResources = do.DeployedOutputResources
return nil
}
diff --git a/pkg/portableresources/backend/controller/createorupdateresource_test.go b/pkg/portableresources/backend/controller/createorupdateresource_test.go
index 3b9732103b..f47a801e44 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource_test.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource_test.go
@@ -61,7 +61,6 @@ func (r *TestResource) ApplyDeploymentOutput(do rpv1.DeploymentOutput) error {
r.Properties.Status.OutputResources = do.DeployedOutputResources
r.ComputedValues = do.ComputedValues
r.SecretValues = do.SecretValues
-
return nil
}
diff --git a/pkg/rp/v1/localids.go b/pkg/rp/v1/localids.go
index 67e7d06344..1d0507bbe0 100644
--- a/pkg/rp/v1/localids.go
+++ b/pkg/rp/v1/localids.go
@@ -47,31 +47,14 @@ import (
const (
LocalIDAzureCosmosAccount = "AzureCosmosAccount"
LocalIDAzureCosmosDBMongo = "AzureCosmosDBMongo"
- LocalIDAzureCosmosDBSQL = "AzureCosmosDBSQL"
- LocalIDAzureFileShare = "AzureFileShare"
LocalIDAzureFileShareStorageAccount = "AzureFileShareStorageAccount"
- LocalIDAzureRedis = "AzureRedis"
- LocalIDAzureServiceBusNamespace = "AzureServiceBusNamespace"
- LocalIDAzureServiceBusQueue = "AzureServiceBusQueue"
- LocalIDAzureSqlServer = "AzureSqlServer"
- LocalIDAzureSqlServerDatabase = "AzureSqlServerDatabase"
- LocalIDExtender = "Extender"
LocalIDDaprStateStoreAzureStorage = "DaprStateStoreAzureStorage"
- LocalIDAzureStorageTableService = "AzureStorageTableService"
- LocalIDAzureStorageTable = "AzureStorageTable"
- LocalIDDaprStateStoreComponent = "DaprStateStoreComponent"
- LocalIDDaprStateStoreSQLServer = "DaprStateStoreSQLServer"
- LocalIDDaprComponent = "DaprComponent"
+ LocalIDDaprSecretStoreAzureKeyVault = "DaprSecretStoreAzureKeyVault"
+ LocalIDDaprPubSubBrokerKafka = "DaprPubSubBrokerKafka"
LocalIDDeployment = "Deployment"
LocalIDGateway = "Gateway"
LocalIDHttpRoute = "HttpRoute"
LocalIDKeyVault = "KeyVault"
- LocalIDRabbitMQDeployment = "KubernetesRabbitMQDeployment"
- LocalIDRabbitMQSecret = "KubernetesRabbitMQSecret"
- LocalIDRabbitMQService = "KubernetesRabbitMQService"
- LocalIDRedisDeployment = "KubernetesRedisDeployment"
- LocalIDRedisService = "KubernetesRedisService"
- LocalIDScrapedSecret = "KubernetesScrapedSecret"
LocalIDSecret = "Secret"
LocalIDConfigMap = "ConfigMap"
LocalIDSecretProviderClass = "SecretProviderClass"
@@ -79,15 +62,12 @@ const (
LocalIDKubernetesRole = "KubernetesRole"
LocalIDKubernetesRoleBinding = "KubernetesRoleBinding"
LocalIDService = "Service"
- LocalIDStatefulSet = "StatefulSet"
LocalIDUserAssignedManagedIdentity = "UserAssignedManagedIdentity"
LocalIDFederatedIdentity = "FederatedIdentity"
LocalIDRoleAssignmentPrefix = "RoleAssignment"
// Obsolete when we remove AppModelV1
- LocalIDRoleAssignmentKVKeys = "RoleAssignment-KVKeys"
- LocalIDRoleAssignmentKVSecretsCerts = "RoleAssignment-KVSecretsCerts"
- LocalIDKeyVaultSecret = "KeyVaultSecret"
+ LocalIDRoleAssignmentKVKeys = "RoleAssignment-KVKeys"
)
// NewLocalID generates a unique string based on the input parameter ids using a stable hashing algorithm.
From 35a3de0007fb989c137f2c72f2878d8ac137455c Mon Sep 17 00:00:00 2001
From: vinayada1 <28875764+vinayada1@users.noreply.github.com>
Date: Wed, 6 Sep 2023 15:35:21 -0700
Subject: [PATCH 05/13] Docs cleanup (#6229)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Removing internal references from ucp docs
## Type of change
- This pull request fixes a bug in Radius and has an approved issue .
Fixes: https://github.com/radius-project/radius/issues/6228
## Auto-generated summary
### ๐ค Generated by Copilot at 85b214f
### Summary
:arrows_counterclockwise::wastebasket::memo:
Updated the UCP documentation to reflect the latest design and usage of
state storage and credentials. Deleted an unused file and fixed some
broken links.
> _`rad credential`_
> _Manage secrets with commands_
> _Design docs no more_
### Walkthrough
* Update the link to the state storage design document in
`docs/ucp/aws.md`
([link](https://github.com/radius-project/radius/pull/6229/files?diff=unified&w=0#diff-ab05297ef5912c3fe9af8990ae991bb0e2b60ee1f432779562332039c5409665L64-R64))
* Replace the credential design document reference with a summary of the
"rad credential" CLI commands in `docs/ucp/resources.md`
([link](https://github.com/radius-project/radius/pull/6229/files?diff=unified&w=0#diff-855597a63f268673bfb989106b82aa9424c7167e7d44c789037ca2f91bf72946L11-R11))
* Delete the outdated file `docs/ucp/references.md`
([link](https://github.com/radius-project/radius/pull/6229/files?diff=unified&w=0#diff-3867e2268f892fa7cea1760b415832784b49be0c5c76362048853be5fb90ea43))
---
docs/ucp/aws.md | 2 +-
docs/ucp/references.md | 9 ---------
docs/ucp/resources.md | 2 +-
3 files changed, 2 insertions(+), 11 deletions(-)
delete mode 100644 docs/ucp/references.md
diff --git a/docs/ucp/aws.md b/docs/ucp/aws.md
index dda809aa63..ef855b5430 100644
--- a/docs/ucp/aws.md
+++ b/docs/ucp/aws.md
@@ -61,4 +61,4 @@ Many AWS resources have a generated name and the resource schema does not necess
To address this issue, we will introduce state storage in UCP. The user will specify a friendly name for the resource in the bicep file that is unique in the deployment scope (which will be the Radius resource group). UCP will create a mapping between the friendly name and the actual AWS resource deployed. After this point, UCP will use this mapping to determine if the resource with the particular friendly name is being created or updated.
-The details of this design can be found at: https://microsoft.sharepoint.com/:w:/t/radiuscoreteam/Ef0J0DM89-1Foyb36i4_a_EBn4zW61Dk8paVfJ9p9RUDOg?e=9tnaV1
+The details of this design can be found at: https://github.com/radius-project/design-notes/pull/21
diff --git a/docs/ucp/references.md b/docs/ucp/references.md
deleted file mode 100644
index 5a758899f2..0000000000
--- a/docs/ucp/references.md
+++ /dev/null
@@ -1,9 +0,0 @@
-## References
-
-[UCP Summary](https://microsoft.sharepoint.com/:w:/r/teams/radiuscoreteam/_layouts/15/Doc.aspx?sourcedoc=%7B00979177-9BF7-4D93-A730-B1CC5AB55E3E%7D&file=2022-04%20UCP%20Summary.docx&action=default&mobileredirect=true&share=IQF3kZcA95uTTacwscxatV4-AVk2opS7WrAGFzv-sQ0k0Do)
-
-[UCP Vision](https://microsoft.sharepoint.com/:w:/r/teams/radiuscoreteam/_layouts/15/Doc.aspx?sourcedoc=%7B217B9C83-6D8D-47EC-AFDE-537FC1A20D27%7D&file=2022-04-01%20UCP%20Vision.docx&action=default&mobileredirect=true&share=IQGDnHshjW3sR6_eU3_Bog0nAfOFmzuio1X1alZMQvxukxU)
-
-[AWS Non-Idempotency Design](https://microsoft-my.sharepoint.com/:w:/p/willsmith/EWqUj9lGHL9Dk4s6aGfXVmQB0K9JicgimbP1gw8QRoAtiQ?e=TJvOhw)
-
-[UCP Credentials Design](https://microsoft.sharepoint.com/:w:/t/radiuscoreteam/EVAuQrRK6tRIqiOZmjnyxjoBUfaa2jF2uiV-jhibg5qB5A?e=2t2hef)
\ No newline at end of file
diff --git a/docs/ucp/resources.md b/docs/ucp/resources.md
index af4001a948..8df4aaf6bb 100644
--- a/docs/ucp/resources.md
+++ b/docs/ucp/resources.md
@@ -8,4 +8,4 @@ UCP uses a Plane resource to support ids that come from different types of syste
A resource group is used to organize user resources. Note that even though conceptually this is similar to an Azure resource group but it is not the same and is a UCP resource independent of Azure.
### Credentials
-A user can configure provider credentials in UCP. Currently Azure and AWS credentials are supported. Please refer to [Credential Design Document](https://microsoft.sharepoint.com/:w:/t/radiuscoreteam/EVAuQrRK6tRIqiOZmjnyxjoBUfaa2jF2uiV-jhibg5qB5A?e=2t2hef) for details.
+A user can configure provider credentials in UCP. Currently Azure and AWS credentials are supported and can be managed using "rad credential" CLI commands.
From f28f8c11a862aaa9dbdb50308466b27c8ffd2a3a Mon Sep 17 00:00:00 2001
From: Will Smith
Date: Thu, 7 Sep 2023 09:11:16 -0700
Subject: [PATCH 06/13] Add purge AWS resources GitHub workflow (#6160)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
* Adding GitHub workflow to purge AWS test resources
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #issue_number
## Auto-generated summary
### ๐ค Generated by Copilot at 60d995b
### Summary
:wastebasket::cloud::runner:
Added a GitHub workflow to purge AWS test resources on demand. The
workflow uses a self-hosted runner and the AWS Cloud Control API to
delete resources of specified types from AWS.
> _`purge-aws` workflow_
> _sweeps away test resources_
> _autumn leaves of code_
### Walkthrough
* Create a new GitHub workflow to purge AWS test resources on demand
([link](https://github.com/project-radius/radius/pull/6160/files?diff=unified&w=0#diff-cdbef2239fc7a1aa34d773cb6dae3395193da5015c322ef49bb02025b701a0bcR1-R43))
---------
Co-authored-by: Young Bu Park
---
.../workflows/purge-aws-test-resources.yaml | 46 +++++++++++++++++++
1 file changed, 46 insertions(+)
create mode 100644 .github/workflows/purge-aws-test-resources.yaml
diff --git a/.github/workflows/purge-aws-test-resources.yaml b/.github/workflows/purge-aws-test-resources.yaml
new file mode 100644
index 0000000000..b2effb7e9d
--- /dev/null
+++ b/.github/workflows/purge-aws-test-resources.yaml
@@ -0,0 +1,46 @@
+# ------------------------------------------------------------
+# 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.
+# ------------------------------------------------------------
+
+name: Purge AWS test resources
+on:
+ workflow_dispatch:
+ schedule:
+ # Run at 12:00AM PST every day.
+ - cron: "0 7 * * 0-6"
+
+env:
+ AWS_REGION: us-west-2
+ AWS_RESOURCE_TYPES: 'AWS::Kinesis::Stream,AWS::S3::Bucket,AWS::RDS::DBInstance,AWS::RDS::DBSubnetGroup,AWS::MemoryDB::Cluster,AWS::MemoryDB::SubnetGroup'
+jobs:
+ purge_aws_resources:
+ name: AWS resources clean-ups
+ runs-on: ubuntu-latest
+ steps:
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v1
+ with:
+ aws-access-key-id: ${{ secrets.FUNCTEST_AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.FUNCTEST_AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ env.AWS_REGION }}
+ - name: Filter and delete resources
+ run: |
+ for resource_type in ${${{env.AWS_RESOURCE_TYPES}}//,/ }
+ do
+ aws cloudcontrol list-resources --type-name "$resource_type" --query "ResourceDescriptions[].Identifier" --output text | tr '\t' '\n' | while read identifier
+ do
+ aws cloudcontrol delete-resource --type-name "$resource_type" --identifier "$identifier"
+ done
+ done
From 90a12a089ef0c33c4815121d4f0f9263b9a76b01 Mon Sep 17 00:00:00 2001
From: Lakshmi Javadekar <103459615+lakshmimsft@users.noreply.github.com>
Date: Thu, 7 Sep 2023 10:38:54 -0700
Subject: [PATCH 07/13] Updating linktype, link-type, linkrecipe, linkmetadata
constructs (#6211)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Updating constructs of linktype, linkrecipe, linkmetadata to
resourcetype, resourcerecipe and portableresourcemetadata respectively.
link-type -> resource-type
LinkType -> ResourceType
LinkRecipe-> ResourceRecipe
LinkMetadata -> PortableResourceMetadata (ResourceMetadata exists,
containing BasicResourceProperties, across all resources)
## Type of change
- This pull request adds or changes features of Radius and has an
approved issue (#6170 ).
Fixes: #6170
## Auto-generated summary
### ๐ค Generated by Copilot at cdd9975
### Summary
๐๐๐ ๏ธ
This pull request renames the term `link-type` to `resource-type` in the
`radius` CLI and the `corerp` API to align with the new concept of
portable resources. This change affects the commands, flags, structs,
functions, tests, and JSON files related to the `recipe` subcommand and
the `EnvironmentRecipe` and `PortableResource` types. The pull request
also updates the `ResourceRecipe` type to match the API definition and
the extender conversion logic.
> _We are the portable resources, we break the chains of link-type_
> _We rise above the old constraints, we forge our own resource-type_
> _We are the portable resources, we extend and adapt to any shape_
> _We are the future of the radius, we are the masters of our fate_
### Walkthrough
* Rename `LinkType` to `ResourceType` in various structs, functions, and
constants to reflect the new terminology for portable resources
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bf4e917446c73565ee6967f28bdd8e619c0d4fa40b215f195b840c021dd36ce9L44-R44),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bf4e917446c73565ee6967f28bdd8e619c0d4fa40b215f195b840c021dd36ce9L542-R548),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-634496167e8df4561426e60325b16d951bcf89d5595715b559a6a008b6f04896L128-R128),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-634496167e8df4561426e60325b16d951bcf89d5595715b559a6a008b6f04896L136-R136),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-455cce87d0a66ee8bd33b2e8549200653478b7e582745b3976b77200d45aed91L106-R106),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-455cce87d0a66ee8bd33b2e8549200653478b7e582745b3976b77200d45aed91L112-R112),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L92-R92),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L195-R198),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL178-R178),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL247-R247),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL274-R274),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL327-R327),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL383-R383),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL435-R435),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-a6f10a0125ba5ff86166bb7dd0369942834185ce5533c250891f995dae6ed236L84-R84),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-a6f10a0125ba5ff86166bb7dd0369942834185ce5533c250891f995dae6ed236L155-R155),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-a6f10a0125ba5ff86166bb7dd0369942834185ce5533c250891f995dae6ed236L162-R162),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L114-R114),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L148-R148),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L190-R190),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L225-R225),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-dcebf4c2ef0dbafd80cf2712748894d06dbe1fd48e2136b3cf3ac99848dad9c9L21-R21),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL68-R68),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL105-R109),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL125-R132),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL139-R139),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL205-R205),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL250-R250),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL300-R300),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL338-R338),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL368-R368),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL421-R421),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f2e80353ac2e49da41bc04958b9568c662162f7dbb52ada2c328851436925482L214-R214),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-d649987c28f8f2d588094e6f264f56f09cce3c06470e5942fd258127b08544dfL69-R69),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-148a9d17d0f5b5371877028809ef405378ccdc9f45782cb92b299f0059422c17L51-R52),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-7b363611c8c65a48f1bf2081dcccb8b96f71e7fd06d0b6bf82b261d62c193d65L92-R93),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-3497c5a60d626f25bd22acce0b5869eafda20f9dd890089f7192ebae77dfd19aL119-R119),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-3497c5a60d626f25bd22acce0b5869eafda20f9dd890089f7192ebae77dfd19aL126-R132),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-5d7486ace43eb3f6c18a9d9336afd5503a0186c9a6f73130a25f5a60f5eccdd1L2-L1))
* Rename `RequireLinkType` to `GetResourceType` and update its logic to
return the resource type instead of requiring it as a flag
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bf4e917446c73565ee6967f28bdd8e619c0d4fa40b215f195b840c021dd36ce9L542-R548),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L133-R137),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-a6f10a0125ba5ff86166bb7dd0369942834185ce5533c250891f995dae6ed236L126-R130),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL105-R109))
* Rename `AddLinkTypeFlag` to `AddResourceTypeFlag` and update its
description and default value to match the new terminology for portable
resources
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-40d092c037a2ba62ae8e3716ee0ebc4e4244023c7b030a0c855da629076a5053L83-R85))
* Rename `--link-type` flag to `--resource-type` flag and update its
usage in various commands and tests
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L55-R61),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L76-R77),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL50-R50),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL59-R59),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL68-R68),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL77-R77),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL86-R86),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL95-R95),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL104-R104),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-b44b694e65d357babf13fbdd996633e13398552ecb5ca8aced2ad4d7bb0e405cL112-R112),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-a6f10a0125ba5ff86166bb7dd0369942834185ce5533c250891f995dae6ed236L71-R72),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L49-R49),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L58-R58),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L67-R67),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L76-R76),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-13fc0c59e89a1fa92926d4fdd0ef9a6ef372c9816c6c43ec6b003427b63e0852L84-R84),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL38-R38),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL55-R56),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL50-R50),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL59-R59),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL77-R77),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL85-R85),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL307-R307),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-af989492b2507d305e426234d48b6905a2d3f4d22e9069d9f3a32c72cbc5d837L251-R252),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-2d2e076adef117e743fd5a3e96df002c933180cf97a8875fcdc04a1c7472644a),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-5d7486ace43eb3f6c18a9d9336afd5503a0186c9a6f73130a25f5a60f5eccdd1L2-L1))
* Rename `LinkRecipe` to `ResourceRecipe` and update its usage in the
`PortableResource` struct and related functions
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-3497c5a60d626f25bd22acce0b5869eafda20f9dd890089f7192ebae77dfd19aL119-R119),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-3497c5a60d626f25bd22acce0b5869eafda20f9dd890089f7192ebae77dfd19aL126-R132),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bdb2119eb3545d9d4248e90cc22d8e305745c34e0261c27993de2c1df6c32274L65-R65),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bdb2119eb3545d9d4248e90cc22d8e305745c34e0261c27993de2c1df6c32274L94-R94),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-bdb2119eb3545d9d4248e90cc22d8e305745c34e0261c27993de2c1df6c32274L122-R122))
* Rename file `environmentresource-invalid-linktype.json` to
`environmentresource-invalid-resourcetype.json` and update its content
to use the `resourceType` field instead of the `linkType` field
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-2d2e076adef117e743fd5a3e96df002c933180cf97a8875fcdc04a1c7472644a))
* Update comments and error messages to mention the `resource-type` flag
instead of the `link-type` flag
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-cd6c2e17113715641f64f2653287de4acb9fe04b45ce8c597ef104ed055d8a16L109-R109),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL38-R38),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-1b915dfcc5ae81097b37f20414aeafc05f8bd67648aaa37e5490ffe8a2c6c08dL83-R83),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-d649987c28f8f2d588094e6f264f56f09cce3c06470e5942fd258127b08544dfL69-R69),[link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-af989492b2507d305e426234d48b6905a2d3f4d22e9069d9f3a32c72cbc5d837L251-R252))
* Update the JSONPath for the `TYPE` column in the
`GetEnvironmentRecipesTableFormat` function to use the `ResourceType`
field instead of the `LinkType` field
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f2e80353ac2e49da41bc04958b9568c662162f7dbb52ada2c328851436925482L214-R214))
* Update the test name for the `recipe unregister` command to mention
the `resource-type` flag instead of the `link-type` flag
([link](https://github.com/radius-project/radius/pull/6211/files?diff=unified&w=0#diff-f77272892bbdeda953e0ee83c7e1199ed43ee540c1e08022845824a36b56ae1fL307-R307))
---
pkg/cli/clivalidation.go | 12 ++++----
pkg/cli/cmd/commonflags/flags.go | 6 ++--
pkg/cli/cmd/recipe/list/list.go | 4 +--
pkg/cli/cmd/recipe/list/list_test.go | 4 +--
pkg/cli/cmd/recipe/register/register.go | 22 +++++++--------
pkg/cli/cmd/recipe/register/register_test.go | 28 +++++++++----------
pkg/cli/cmd/recipe/show/show.go | 14 +++++-----
pkg/cli/cmd/recipe/show/show_test.go | 18 ++++++------
pkg/cli/cmd/recipe/types.go | 2 +-
pkg/cli/cmd/recipe/unregister/unregister.go | 20 ++++++-------
.../cmd/recipe/unregister/unregister_test.go | 24 ++++++++--------
pkg/cli/objectformats/objectformats.go | 2 +-
.../environment_conversion.go | 2 +-
.../environment_conversion_test.go | 4 +--
.../environmentrecipeproperties_conversion.go | 4 +--
...ronmentrecipeproperties_conversion_test.go | 4 +--
.../extender_conversion.go | 8 +++---
.../extender_conversion_test.go | 6 ++--
...ronmentresource-invalid-resourcetype.json} | 0
.../testdata/reciperesource.json | 2 +-
.../zz_generated_models.go | 6 ++--
.../zz_generated_models_serde.go | 8 +++---
.../deployment/deploymentprocessor_test.go | 6 ++--
pkg/corerp/datamodel/container.go | 2 +-
pkg/corerp/datamodel/environment.go | 2 +-
pkg/corerp/datamodel/extender.go | 10 +++----
pkg/corerp/datamodel/gateway.go | 2 +-
pkg/corerp/datamodel/httproute.go | 2 +-
...etadata.go => portableresourcemetadata.go} | 4 +--
pkg/corerp/datamodel/secretstore.go | 2 +-
pkg/corerp/datamodel/volume.go | 2 +-
.../environments/getrecipemetadata.go | 6 ++--
.../environments/getrecipemetadata_test.go | 6 ++--
...ingrecipe20220315privatepreview_input.json | 2 +-
...emetadata20220315privatepreview_input.json | 2 +-
...0220315privatepreview_input_terraform.json | 2 +-
.../v20220315privatepreview/datamodel_util.go | 8 +++---
.../datamodel_util_test.go | 8 +++---
.../pubsubbroker_conversion_test.go | 2 +-
.../secretstore_conversion_test.go | 2 +-
pkg/daprrp/datamodel/daprpubsubbroker.go | 8 +++---
pkg/daprrp/datamodel/daprsecretstore.go | 8 +++---
pkg/daprrp/datamodel/daprstatestore.go | 8 +++---
.../v20220315privatepreview/datamodel_util.go | 8 +++---
.../datamodel_util_test.go | 12 ++++----
.../mongodatabase_conversion_test.go | 4 +--
.../rediscache_conversion_test.go | 6 ++--
.../sqldatabase_conversion_test.go | 2 +-
pkg/datastoresrp/datamodel/mongodatabase.go | 10 +++----
pkg/datastoresrp/datamodel/rediscache.go | 10 +++----
pkg/datastoresrp/datamodel/sqldatabase.go | 10 +++----
.../v20220315privatepreview/datamodel_util.go | 8 +++---
.../datamodel_util_test.go | 8 +++---
.../rabbitmq_conversion_test.go | 2 +-
pkg/messagingrp/datamodel/rabbitmq.go | 10 +++----
.../controller/createorupdateresource_test.go | 12 ++++----
.../{linkmetadata.go => metadata.go} | 23 +++++----------
pkg/portableresources/datamodel/recipes.go | 2 +-
pkg/portableresources/types.go | 16 +++++------
pkg/recipes/recipecontext/context.go | 2 +-
pkg/recipes/recipecontext/types.go | 2 +-
pkg/rp/doc.go | 2 +-
.../Environments_GetRecipeMetadata.json | 2 +-
.../2022-03-15-privatepreview/openapi.json | 6 ++--
.../examples/Planes_GetPlaneLocal.json | 2 +-
.../examples/Planes_List.json | 2 +-
.../examples/Planes_ListPlanesByType.json | 2 +-
test/functional/shared/cli/cli_test.go | 26 ++++++++---------
test/radcli/cli.go | 18 ++++++------
typespec/Applications.Core/environments.tsp | 4 +--
.../Environments_GetRecipeMetadata.json | 2 +-
.../Planes_GetPlaneLocal.json | 2 +-
.../Planes_List.json | 2 +-
.../Planes_ListPlanesByType.json | 2 +-
74 files changed, 252 insertions(+), 261 deletions(-)
rename pkg/corerp/api/v20220315privatepreview/testdata/{environmentresource-invalid-linktype.json => environmentresource-invalid-resourcetype.json} (100%)
rename pkg/corerp/datamodel/{linkmetadata.go => portableresourcemetadata.go} (88%)
rename pkg/portableresources/datamodel/{linkmetadata.go => metadata.go} (66%)
diff --git a/pkg/cli/clivalidation.go b/pkg/cli/clivalidation.go
index 195698d9be..bc8e317995 100644
--- a/pkg/cli/clivalidation.go
+++ b/pkg/cli/clivalidation.go
@@ -41,7 +41,7 @@ type AzureResource struct {
}
const (
- LinkTypeFlag = "link-type"
+ ResourceTypeFlag = "resource-type"
)
// RequireEnvironmentNameArgs checks if an environment name is provided as an argument or if a default environment is set
@@ -539,11 +539,11 @@ func RequireRecipeNameArgs(cmd *cobra.Command, args []string) (string, error) {
return args[0], nil
}
-// RequireLinkType retrieves the link type flag from the given command and returns it, or an error if the flag is not set.
-func RequireLinkType(cmd *cobra.Command) (string, error) {
- linkType, err := cmd.Flags().GetString(LinkTypeFlag)
+// GetResourceType retrieves the resource type flag from the given command and returns it, or an error if the flag is not set.
+func GetResourceType(cmd *cobra.Command) (string, error) {
+ resourceType, err := cmd.Flags().GetString(ResourceTypeFlag)
if err != nil {
- return linkType, err
+ return resourceType, err
}
- return linkType, nil
+ return resourceType, nil
}
diff --git a/pkg/cli/cmd/commonflags/flags.go b/pkg/cli/cmd/commonflags/flags.go
index 839ed52acf..ecc84172ac 100644
--- a/pkg/cli/cmd/commonflags/flags.go
+++ b/pkg/cli/cmd/commonflags/flags.go
@@ -80,9 +80,9 @@ func AddParameterFlag(cmd *cobra.Command) {
cmd.Flags().StringArrayP("parameters", "p", []string{}, "Specify parameters for the deployment")
}
-// AddLinkTypeFlag adds a flag to the given command that allows the user to specify the type of the link this recipe can be consumed by.
-func AddLinkTypeFlag(cmd *cobra.Command) {
- cmd.Flags().String("link-type", "", "Specify the type of the link this recipe can be consumed by")
+// AddResourceTypeFlag adds a flag to the given command that allows the user to specify the type of the portable resource this recipe can be consumed by.
+func AddResourceTypeFlag(cmd *cobra.Command) {
+ cmd.Flags().String("resource-type", "", "Specify the type of the portable resource this recipe can be consumed by")
}
// AddAzureScopeFlags adds flags to a command to specify an Azure subscription and resource group, and marks them as
diff --git a/pkg/cli/cmd/recipe/list/list.go b/pkg/cli/cmd/recipe/list/list.go
index 4974b7b656..fd5797c0c1 100644
--- a/pkg/cli/cmd/recipe/list/list.go
+++ b/pkg/cli/cmd/recipe/list/list.go
@@ -125,7 +125,7 @@ func (r *Runner) Run(ctx context.Context) error {
case *corerp.TerraformRecipeProperties:
recipe = types.EnvironmentRecipe{
Name: recipeName,
- LinkType: resourceType,
+ ResourceType: resourceType,
TemplatePath: *c.TemplatePath,
TemplateKind: *c.TemplateKind,
TemplateVersion: *c.TemplateVersion,
@@ -133,7 +133,7 @@ func (r *Runner) Run(ctx context.Context) error {
case *corerp.BicepRecipeProperties:
recipe = types.EnvironmentRecipe{
Name: recipeName,
- LinkType: resourceType,
+ ResourceType: resourceType,
TemplatePath: *c.TemplatePath,
TemplateKind: *c.TemplateKind,
}
diff --git a/pkg/cli/cmd/recipe/list/list_test.go b/pkg/cli/cmd/recipe/list/list_test.go
index 14e195d0da..95da164d7c 100644
--- a/pkg/cli/cmd/recipe/list/list_test.go
+++ b/pkg/cli/cmd/recipe/list/list_test.go
@@ -103,13 +103,13 @@ func Test_Run(t *testing.T) {
recipes := []types.EnvironmentRecipe{
{
Name: "cosmosDB",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
TemplateKind: recipes.TemplateKindBicep,
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1",
},
{
Name: "cosmosDB-terraform",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
TemplateKind: recipes.TemplateKindTerraform,
TemplatePath: "Azure/cosmosdb/azurerm",
TemplateVersion: "1.1.0",
diff --git a/pkg/cli/cmd/recipe/register/register.go b/pkg/cli/cmd/recipe/register/register.go
index 77a0fdedb0..cd49b946db 100644
--- a/pkg/cli/cmd/recipe/register/register.go
+++ b/pkg/cli/cmd/recipe/register/register.go
@@ -52,13 +52,13 @@ You can specify parameters using the '--parameter' flag ('-p' for short). Parame
`,
Example: `
# Add a recipe to an environment
-rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --link-type Applications.Datastores/mongoDatabases
+rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --resource-type Applications.Datastores/mongoDatabases
# Specify a parameter
-rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --link-type Applications.Datastores/mongoDatabases --parameters throughput=400
+rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --resource-type Applications.Datastores/mongoDatabases --parameters throughput=400
# specify multiple parameters using a JSON parameter file
-rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --link-type Applications.Datastores/mongoDatabases --parameters @myfile.json
+rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --template-path template_path --resource-type Applications.Datastores/mongoDatabases --parameters @myfile.json
`,
Args: cobra.ExactArgs(1),
RunE: framework.RunCommand(runner),
@@ -73,8 +73,8 @@ rad recipe register cosmosdb -e env_name -w workspace --template-kind bicep --te
cmd.Flags().String("template-version", "", "specify the version for the terraform module.")
cmd.Flags().String("template-path", "", "specify the path to the template provided by the recipe.")
_ = cmd.MarkFlagRequired("template-path")
- cmd.Flags().String("link-type", "", "specify the type of the portable resource this recipe can be consumed by")
- _ = cmd.MarkFlagRequired("link-type")
+ cmd.Flags().String("resource-type", "", "specify the type of the portable resource this recipe can be consumed by")
+ _ = cmd.MarkFlagRequired("resource-type")
commonflags.AddParameterFlag(cmd)
return cmd, runner
@@ -89,7 +89,7 @@ type Runner struct {
TemplateKind string
TemplatePath string
TemplateVersion string
- LinkType string
+ ResourceType string
RecipeName string
Parameters map[string]map[string]any
}
@@ -106,7 +106,7 @@ func NewRunner(factory framework.Factory) *Runner {
// Validate runs validation for the `rad recipe register` command.
//
-// Validate validates the command line args, sets the workspace, environment, template kind, template path, link type,
+// Validate validates the command line args, sets the workspace, environment, template kind, template path, resource type,
// recipe name, and parameters, and returns an error if any of these fail.
func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
// Validate command line args
@@ -130,11 +130,11 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
r.TemplatePath = templatePath
r.TemplateVersion = templateVersion
- linkType, err := cli.RequireLinkType(cmd)
+ resourceType, err := cli.GetResourceType(cmd)
if err != nil {
return err
}
- r.LinkType = linkType
+ r.ResourceType = resourceType
recipeName, err := cli.RequireRecipeNameArgs(cmd, args)
if err != nil {
@@ -192,10 +192,10 @@ func (r *Runner) Run(ctx context.Context) error {
Parameters: bicep.ConvertToMapStringInterface(r.Parameters),
}
}
- if val, ok := envRecipes[r.LinkType]; ok {
+ if val, ok := envRecipes[r.ResourceType]; ok {
val[r.RecipeName] = properties
} else {
- envRecipes[r.LinkType] = map[string]corerp.RecipePropertiesClassification{
+ envRecipes[r.ResourceType] = map[string]corerp.RecipePropertiesClassification{
r.RecipeName: properties,
}
}
diff --git a/pkg/cli/cmd/recipe/register/register_test.go b/pkg/cli/cmd/recipe/register/register_test.go
index cb84b910d6..ac66ccf7bc 100644
--- a/pkg/cli/cmd/recipe/register/register_test.go
+++ b/pkg/cli/cmd/recipe/register/register_test.go
@@ -47,7 +47,7 @@ func Test_Validate(t *testing.T) {
testcases := []radcli.ValidateInput{
{
Name: "Valid Register Command with parameters",
- Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType, "--parameters", "a=b"},
+ Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType, "--parameters", "a=b"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -56,7 +56,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Valid Register Command for terraform recipe",
- Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindTerraform, "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType, "--template-version", "1.1.0"},
+ Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindTerraform, "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType, "--template-version", "1.1.0"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -65,7 +65,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Valid Register Command with parameters passed as file",
- Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType, "--parameters", "@testdata/recipeparam.json"},
+ Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType, "--parameters", "@testdata/recipeparam.json"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -74,7 +74,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Register Command with fallback workspace",
- Input: []string{"-e", "myenvironment", "test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType},
+ Input: []string{"-e", "myenvironment", "test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -83,7 +83,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Register Command without name",
- Input: []string{"--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType},
+ Input: []string{"--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -92,7 +92,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Register Command without template kind",
- Input: []string{"test_recipe", "--template-path", "test_template", "--link-type", portableresources.MongoDatabasesResourceType},
+ Input: []string{"test_recipe", "--template-path", "test_template", "--resource-type", portableresources.MongoDatabasesResourceType},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -101,7 +101,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Register Command without template path",
- Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--link-type", portableresources.MongoDatabasesResourceType},
+ Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--resource-type", portableresources.MongoDatabasesResourceType},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -109,7 +109,7 @@ func Test_Validate(t *testing.T) {
},
},
{
- Name: "Register Command without link-type",
+ Name: "Register Command without resource-type",
Input: []string{"test_recipe", "--template-kind", recipes.TemplateKindBicep, "--template-path", "test_template"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
@@ -175,7 +175,7 @@ func Test_Run(t *testing.T) {
TemplateKind: recipes.TemplateKindTerraform,
TemplatePath: "Azure/cosmosdb/azurerm",
TemplateVersion: "1.1.0",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
RecipeName: "cosmosDB_new",
}
@@ -244,7 +244,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
RecipeName: "cosmosDB_new",
}
@@ -271,7 +271,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
RecipeName: "cosmosDB_new",
}
@@ -324,7 +324,7 @@ func Test_Run(t *testing.T) {
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
TemplateKind: recipes.TemplateKindBicep,
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/rediscaches:v1",
- LinkType: portableresources.RedisCachesResourceType,
+ ResourceType: portableresources.RedisCachesResourceType,
RecipeName: "redis",
Parameters: map[string]map[string]any{},
}
@@ -380,7 +380,7 @@ func Test_Run(t *testing.T) {
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
TemplateKind: recipes.TemplateKindBicep,
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
RecipeName: "cosmosDB_no_namespace",
}
@@ -432,7 +432,7 @@ func Test_Run(t *testing.T) {
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
TemplateKind: recipes.TemplateKindBicep,
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/rediscaches:v1",
- LinkType: portableresources.RedisCachesResourceType,
+ ResourceType: portableresources.RedisCachesResourceType,
RecipeName: "redis",
}
diff --git a/pkg/cli/cmd/recipe/show/show.go b/pkg/cli/cmd/recipe/show/show.go
index 6698acbe7a..b051cd0050 100644
--- a/pkg/cli/cmd/recipe/show/show.go
+++ b/pkg/cli/cmd/recipe/show/show.go
@@ -68,8 +68,8 @@ rad recipe show redis-dev --group dev --environment dev`,
commonflags.AddWorkspaceFlag(cmd)
commonflags.AddResourceGroupFlag(cmd)
commonflags.AddEnvironmentNameFlag(cmd)
- commonflags.AddLinkTypeFlag(cmd)
- _ = cmd.MarkFlagRequired(cli.LinkTypeFlag)
+ commonflags.AddResourceTypeFlag(cmd)
+ _ = cmd.MarkFlagRequired(cli.ResourceTypeFlag)
return cmd, runner
}
@@ -81,7 +81,7 @@ type Runner struct {
Output output.Interface
Workspace *workspaces.Workspace
RecipeName string
- LinkType string
+ ResourceType string
Format string
}
@@ -123,11 +123,11 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
}
r.RecipeName = recipeName
- linkType, err := cli.RequireLinkType(cmd)
+ resourceType, err := cli.GetResourceType(cmd)
if err != nil {
return err
}
- r.LinkType = linkType
+ r.ResourceType = resourceType
format, err := cli.RequireOutput(cmd)
if err != nil {
@@ -152,14 +152,14 @@ func (r *Runner) Run(ctx context.Context) error {
return err
}
- recipeDetails, err := client.ShowRecipe(ctx, r.Workspace.Environment, v20220315privatepreview.RecipeGetMetadata{Name: &r.RecipeName, LinkType: &r.LinkType})
+ recipeDetails, err := client.ShowRecipe(ctx, r.Workspace.Environment, v20220315privatepreview.RecipeGetMetadata{Name: &r.RecipeName, ResourceType: &r.ResourceType})
if err != nil {
return err
}
recipe := types.EnvironmentRecipe{
Name: r.RecipeName,
- LinkType: r.LinkType,
+ ResourceType: r.ResourceType,
TemplatePath: *recipeDetails.TemplatePath,
TemplateKind: *recipeDetails.TemplateKind,
}
diff --git a/pkg/cli/cmd/recipe/show/show_test.go b/pkg/cli/cmd/recipe/show/show_test.go
index e489c34390..903ba99381 100644
--- a/pkg/cli/cmd/recipe/show/show_test.go
+++ b/pkg/cli/cmd/recipe/show/show_test.go
@@ -46,7 +46,7 @@ func Test_Validate(t *testing.T) {
testcases := []radcli.ValidateInput{
{
Name: "Valid Show Command",
- Input: []string{"recipeName", "--link-type", "link-type"},
+ Input: []string{"recipeName", "--resource-type", "resource-type"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -55,7 +55,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Show Command with incorrect fallback workspace",
- Input: []string{"-e", "my-env", "-g", "my-env", "recipeName", "--link-type", "link-type"},
+ Input: []string{"-e", "my-env", "-g", "my-env", "recipeName", "--resource-type", "resource-type"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -64,7 +64,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Show Command with too many positional args",
- Input: []string{"recipeName", "arg2", "--link-type", "link-type"},
+ Input: []string{"recipeName", "arg2", "--resource-type", "resource-type"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -73,7 +73,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Show Command with fallback workspace",
- Input: []string{"-e", "my-env", "-w", "test-workspace", "recipeName", "--link-type", "link-type"},
+ Input: []string{"-e", "my-env", "-w", "test-workspace", "recipeName", "--resource-type", "resource-type"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -81,7 +81,7 @@ func Test_Validate(t *testing.T) {
},
},
{
- Name: "Show Command without LinkType",
+ Name: "Show Command without ResourceType",
Input: []string{"recipeName"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
@@ -111,7 +111,7 @@ func Test_Run(t *testing.T) {
}
recipe := types.EnvironmentRecipe{
Name: "cosmosDB",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
TemplateKind: recipes.TemplateKindBicep,
TemplatePath: "testpublicrecipe.azurecr.io/bicep/modules/mongodatabases:v1",
}
@@ -145,7 +145,7 @@ func Test_Run(t *testing.T) {
Workspace: &workspaces.Workspace{},
Format: "table",
RecipeName: "cosmosDB",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
}
err := runner.Run(context.Background())
@@ -187,7 +187,7 @@ func Test_Run(t *testing.T) {
}
recipe := types.EnvironmentRecipe{
Name: "cosmosDB",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
TemplateKind: recipes.TemplateKindTerraform,
TemplatePath: "Azure/cosmosdb/azurerm",
TemplateVersion: "1.1.0",
@@ -222,7 +222,7 @@ func Test_Run(t *testing.T) {
Workspace: &workspaces.Workspace{},
Format: "table",
RecipeName: "cosmosDB",
- LinkType: portableresources.MongoDatabasesResourceType,
+ ResourceType: portableresources.MongoDatabasesResourceType,
}
err := runner.Run(context.Background())
diff --git a/pkg/cli/cmd/recipe/types.go b/pkg/cli/cmd/recipe/types.go
index 6c4ff26ce7..7b8104e84d 100644
--- a/pkg/cli/cmd/recipe/types.go
+++ b/pkg/cli/cmd/recipe/types.go
@@ -18,7 +18,7 @@ package recipe
type EnvironmentRecipe struct {
Name string `json:"name"`
- LinkType string `json:"linkType"`
+ ResourceType string `json:"resourceType"`
TemplateKind string `json:"templateKind"`
TemplatePath string `json:"templatePath"`
TemplateVersion string `json:"templateVersion"`
diff --git a/pkg/cli/cmd/recipe/unregister/unregister.go b/pkg/cli/cmd/recipe/unregister/unregister.go
index c6c8e818f3..0d529503c0 100644
--- a/pkg/cli/cmd/recipe/unregister/unregister.go
+++ b/pkg/cli/cmd/recipe/unregister/unregister.go
@@ -35,7 +35,7 @@ import (
//
// NewCommand creates a new cobra command for unregistering a recipe from an environment, which takes in a factory and returns a cobra command
-// and a runner. It also sets up flags for output, workspace, resource group, environment name and portable resource type, with link-type being a required flag.
+// and a runner. It also sets up flags for output, workspace, resource group, environment name and portable resource type, with resource-type being a required flag.
func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) {
runner := NewRunner(factory)
@@ -52,8 +52,8 @@ func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) {
commonflags.AddWorkspaceFlag(cmd)
commonflags.AddResourceGroupFlag(cmd)
commonflags.AddEnvironmentNameFlag(cmd)
- commonflags.AddLinkTypeFlag(cmd)
- _ = cmd.MarkFlagRequired(cli.LinkTypeFlag)
+ commonflags.AddResourceTypeFlag(cmd)
+ _ = cmd.MarkFlagRequired(cli.ResourceTypeFlag)
return cmd, runner
}
@@ -65,7 +65,7 @@ type Runner struct {
Output output.Interface
Workspace *workspaces.Workspace
RecipeName string
- LinkType string
+ ResourceType string
}
// NewRunner creates a new instance of the `rad recipe unregister` runner.
@@ -80,7 +80,7 @@ func NewRunner(factory framework.Factory) *Runner {
// Validate runs validation for the `rad recipe unregister` command.
//
-// // Runner.Validate checks the command line arguments for a workspace, environment, recipe name, and link type, and
+// // Runner.Validate checks the command line arguments for a workspace, environment, recipe name, and resource type, and
// returns an error if any of these are not present.
func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
// Validate command line args
@@ -102,11 +102,11 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error {
}
r.RecipeName = recipeName
- linkType, err := cli.RequireLinkType(cmd)
+ resourceType, err := cli.GetResourceType(cmd)
if err != nil {
return err
}
- r.LinkType = linkType
+ r.ResourceType = resourceType
return nil
}
@@ -122,14 +122,14 @@ func (r *Runner) Run(ctx context.Context) error {
return err
}
- envResource, recipeProperties, err := cmd.CheckIfRecipeExists(ctx, client, r.Workspace.Environment, r.RecipeName, r.LinkType)
+ envResource, recipeProperties, err := cmd.CheckIfRecipeExists(ctx, client, r.Workspace.Environment, r.RecipeName, r.ResourceType)
if err != nil {
return err
}
- if val, ok := recipeProperties[r.LinkType]; ok {
+ if val, ok := recipeProperties[r.ResourceType]; ok {
delete(val, r.RecipeName)
if len(val) == 0 {
- delete(recipeProperties, r.LinkType)
+ delete(recipeProperties, r.ResourceType)
}
}
envResource.Properties.Recipes = recipeProperties
diff --git a/pkg/cli/cmd/recipe/unregister/unregister_test.go b/pkg/cli/cmd/recipe/unregister/unregister_test.go
index a493fc8fa9..e7b9302202 100644
--- a/pkg/cli/cmd/recipe/unregister/unregister_test.go
+++ b/pkg/cli/cmd/recipe/unregister/unregister_test.go
@@ -47,7 +47,7 @@ func Test_Validate(t *testing.T) {
testcases := []radcli.ValidateInput{
{
Name: "Valid Unregister Command",
- Input: []string{"test_recipe", "--link-type", "link-type"},
+ Input: []string{"test_recipe", "--resource-type", "resource-type"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -56,7 +56,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Unregister Command with fallback workspace",
- Input: []string{"-e", "my-env", "test_recipe", "--link-type", "link-type"},
+ Input: []string{"-e", "my-env", "test_recipe", "--resource-type", "resource-type"},
ExpectedValid: true,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -74,7 +74,7 @@ func Test_Validate(t *testing.T) {
},
{
Name: "Unregister Command with too many args",
- Input: []string{"foo", "bar", "foo1", "--link-type", "link-type"},
+ Input: []string{"foo", "bar", "foo1", "--resource-type", "resource-type"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
ConfigFilePath: "",
@@ -82,7 +82,7 @@ func Test_Validate(t *testing.T) {
},
},
{
- Name: "Unregister Command without link type",
+ Name: "Unregister Command without resource type",
Input: []string{"foo"},
ExpectedValid: false,
ConfigHolder: framework.ConfigHolder{
@@ -136,7 +136,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "cosmosDB",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
expectedOutput := []any{
@@ -202,7 +202,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "cosmosDB",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
err := runner.Run(context.Background())
@@ -247,7 +247,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "cosmosDB",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
expectedOutput := []any{
@@ -297,14 +297,14 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "cosmosDB1",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
err := runner.Run(context.Background())
require.Error(t, err)
})
- t.Run("Unregister recipe with linkType doesn't exist in the environment", func(t *testing.T) {
+ t.Run("Unregister recipe with resourceType doesn't exist in the environment", func(t *testing.T) {
ctrl := gomock.NewController(t)
envResource := v20220315privatepreview.EnvironmentResource{
ID: to.Ptr("/planes/radius/local/resourcegroups/kind-kind/providers/applications.core/environments/kind-kind"),
@@ -335,7 +335,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "testResource",
- LinkType: "Applications.Datastores/redisCaches",
+ ResourceType: "Applications.Datastores/redisCaches",
}
err := runner.Run(context.Background())
@@ -365,7 +365,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "cosmosDB",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
err := runner.Run(context.Background())
@@ -418,7 +418,7 @@ func Test_Run(t *testing.T) {
Output: outputSink,
Workspace: &workspaces.Workspace{Environment: "kind-kind"},
RecipeName: "testResource",
- LinkType: "Applications.Datastores/mongoDatabases",
+ ResourceType: "Applications.Datastores/mongoDatabases",
}
expectedOutput := []any{
diff --git a/pkg/cli/objectformats/objectformats.go b/pkg/cli/objectformats/objectformats.go
index 499bd2c957..b6224cbd6e 100644
--- a/pkg/cli/objectformats/objectformats.go
+++ b/pkg/cli/objectformats/objectformats.go
@@ -211,7 +211,7 @@ func GetEnvironmentRecipesTableFormat() output.FormatterOptions {
},
{
Heading: "TYPE",
- JSONPath: "{ .LinkType }",
+ JSONPath: "{ .ResourceType }",
},
{
Heading: "TEMPLATE KIND",
diff --git a/pkg/corerp/api/v20220315privatepreview/environment_conversion.go b/pkg/corerp/api/v20220315privatepreview/environment_conversion.go
index 1f4610bb70..51101bb4a9 100644
--- a/pkg/corerp/api/v20220315privatepreview/environment_conversion.go
+++ b/pkg/corerp/api/v20220315privatepreview/environment_conversion.go
@@ -66,7 +66,7 @@ func (src *EnvironmentResource) ConvertTo() (v1.DataModelInterface, error) {
envRecipes := make(map[string]map[string]datamodel.EnvironmentRecipeProperties)
for resourceType, recipes := range src.Properties.Recipes {
if !portableresources.IsValidPortableResourceType(resourceType) {
- return &datamodel.Environment{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid link type: %q", resourceType))
+ return &datamodel.Environment{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid resource type: %q", resourceType))
}
envRecipes[resourceType] = map[string]datamodel.EnvironmentRecipeProperties{}
for recipeName, recipeDetails := range recipes {
diff --git a/pkg/corerp/api/v20220315privatepreview/environment_conversion_test.go b/pkg/corerp/api/v20220315privatepreview/environment_conversion_test.go
index 45423b8c3c..42545b43f9 100644
--- a/pkg/corerp/api/v20220315privatepreview/environment_conversion_test.go
+++ b/pkg/corerp/api/v20220315privatepreview/environment_conversion_test.go
@@ -248,8 +248,8 @@ func TestConvertVersionedToDataModel(t *testing.T) {
err: &v1.ErrModelConversion{PropertyName: "$.properties.compute.namespace", ValidValue: "63 characters or less"},
},
{
- filename: "environmentresource-invalid-linktype.json",
- err: &v1.ErrClientRP{Code: v1.CodeInvalid, Message: "invalid link type: \"Applications.Dapr/pubsub\""},
+ filename: "environmentresource-invalid-resourcetype.json",
+ err: &v1.ErrClientRP{Code: v1.CodeInvalid, Message: "invalid resource type: \"Applications.Dapr/pubsub\""},
},
{
filename: "environmentresource-invalid-templatekind.json",
diff --git a/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion.go b/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion.go
index b01962447b..e2dd4c990f 100644
--- a/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion.go
+++ b/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion.go
@@ -48,7 +48,7 @@ func (dst *RecipeGetMetadataResponse) ConvertFrom(src v1.DataModelInterface) err
// ConvertTo converts from the versioned Environment Recipe Properties resource to version-agnostic datamodel.
func (src *RecipeGetMetadata) ConvertTo() (v1.DataModelInterface, error) {
return &datamodel.Recipe{
- Name: to.String(src.Name),
- LinkType: to.String(src.LinkType),
+ Name: to.String(src.Name),
+ ResourceType: to.String(src.ResourceType),
}, nil
}
diff --git a/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion_test.go b/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion_test.go
index ead1d8723e..6f9fa96f6d 100644
--- a/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion_test.go
+++ b/pkg/corerp/api/v20220315privatepreview/environmentrecipeproperties_conversion_test.go
@@ -89,8 +89,8 @@ func TestRecipeConvertVersionedToDataModel(t *testing.T) {
t.Run("Convert to Data Model", func(t *testing.T) {
filename := "reciperesource.json"
expected := &datamodel.Recipe{
- LinkType: portableresources.MongoDatabasesResourceType,
- Name: "mongo-azure",
+ ResourceType: portableresources.MongoDatabasesResourceType,
+ Name: "mongo-azure",
}
rawPayload := testutil.ReadFixture(filename)
r := &RecipeGetMetadata{}
diff --git a/pkg/corerp/api/v20220315privatepreview/extender_conversion.go b/pkg/corerp/api/v20220315privatepreview/extender_conversion.go
index 6c4a593e4c..f5153f9b91 100644
--- a/pkg/corerp/api/v20220315privatepreview/extender_conversion.go
+++ b/pkg/corerp/api/v20220315privatepreview/extender_conversion.go
@@ -116,20 +116,20 @@ func fromResourceProvisioningDataModel(provisioning portableresources.ResourcePr
return &converted
}
-func fromRecipeDataModel(r portableresources.LinkRecipe) *Recipe {
+func fromRecipeDataModel(r portableresources.ResourceRecipe) *Recipe {
return &Recipe{
Name: to.Ptr(r.Name),
Parameters: r.Parameters,
}
}
-func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
+func toRecipeDataModel(r *Recipe) portableresources.ResourceRecipe {
if r == nil {
- return portableresources.LinkRecipe{
+ return portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
}
}
- recipe := portableresources.LinkRecipe{}
+ recipe := portableresources.ResourceRecipe{}
if r.Name == nil {
recipe.Name = portableresources.DefaultRecipeName
} else {
diff --git a/pkg/corerp/api/v20220315privatepreview/extender_conversion_test.go b/pkg/corerp/api/v20220315privatepreview/extender_conversion_test.go
index f79a6f36a7..e308f266fe 100644
--- a/pkg/corerp/api/v20220315privatepreview/extender_conversion_test.go
+++ b/pkg/corerp/api/v20220315privatepreview/extender_conversion_test.go
@@ -62,7 +62,7 @@ func TestExtender_ConvertVersionedToDataModel(t *testing.T) {
AdditionalProperties: map[string]any{"fromNumber": "222-222-2222"},
ResourceProvisioning: portableresources.ResourceProvisioningManual,
Secrets: map[string]any{"accountSid": "sid", "authToken": "token"},
- ResourceRecipe: portableresources.LinkRecipe{Name: "default"},
+ ResourceRecipe: portableresources.ResourceRecipe{Name: "default"},
},
},
},
@@ -91,7 +91,7 @@ func TestExtender_ConvertVersionedToDataModel(t *testing.T) {
},
AdditionalProperties: map[string]any{"fromNumber": "222-222-2222"},
ResourceProvisioning: portableresources.ResourceProvisioningManual,
- ResourceRecipe: portableresources.LinkRecipe{Name: "default"},
+ ResourceRecipe: portableresources.ResourceRecipe{Name: "default"},
},
},
},
@@ -119,7 +119,7 @@ func TestExtender_ConvertVersionedToDataModel(t *testing.T) {
Environment: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/env0",
},
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
- ResourceRecipe: portableresources.LinkRecipe{Name: "test-recipe"},
+ ResourceRecipe: portableresources.ResourceRecipe{Name: "test-recipe"},
},
},
},
diff --git a/pkg/corerp/api/v20220315privatepreview/testdata/environmentresource-invalid-linktype.json b/pkg/corerp/api/v20220315privatepreview/testdata/environmentresource-invalid-resourcetype.json
similarity index 100%
rename from pkg/corerp/api/v20220315privatepreview/testdata/environmentresource-invalid-linktype.json
rename to pkg/corerp/api/v20220315privatepreview/testdata/environmentresource-invalid-resourcetype.json
diff --git a/pkg/corerp/api/v20220315privatepreview/testdata/reciperesource.json b/pkg/corerp/api/v20220315privatepreview/testdata/reciperesource.json
index 31eff8b802..ee608ee6db 100644
--- a/pkg/corerp/api/v20220315privatepreview/testdata/reciperesource.json
+++ b/pkg/corerp/api/v20220315privatepreview/testdata/reciperesource.json
@@ -1,4 +1,4 @@
{
- "linkType":"Applications.Datastores/mongoDatabases",
+ "resourceType":"Applications.Datastores/mongoDatabases",
"name":"mongo-azure"
}
\ No newline at end of file
diff --git a/pkg/corerp/api/v20220315privatepreview/zz_generated_models.go b/pkg/corerp/api/v20220315privatepreview/zz_generated_models.go
index ab9c514a02..707d39a845 100644
--- a/pkg/corerp/api/v20220315privatepreview/zz_generated_models.go
+++ b/pkg/corerp/api/v20220315privatepreview/zz_generated_models.go
@@ -1295,11 +1295,11 @@ type Recipe struct {
// RecipeGetMetadata - Represents the request body of the getmetadata action.
type RecipeGetMetadata struct {
- // REQUIRED; Type of the link this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'
- LinkType *string
-
// REQUIRED; The name of the recipe registered to the environment
Name *string
+
+ // REQUIRED; Type of the resource this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'
+ ResourceType *string
}
// RecipeGetMetadataResponse - The properties of a Recipe linked to an Environment.
diff --git a/pkg/corerp/api/v20220315privatepreview/zz_generated_models_serde.go b/pkg/corerp/api/v20220315privatepreview/zz_generated_models_serde.go
index 8919ce08a3..f56e2e9934 100644
--- a/pkg/corerp/api/v20220315privatepreview/zz_generated_models_serde.go
+++ b/pkg/corerp/api/v20220315privatepreview/zz_generated_models_serde.go
@@ -3048,8 +3048,8 @@ func (r *Recipe) UnmarshalJSON(data []byte) error {
// MarshalJSON implements the json.Marshaller interface for type RecipeGetMetadata.
func (r RecipeGetMetadata) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]any)
- populate(objectMap, "linkType", r.LinkType)
populate(objectMap, "name", r.Name)
+ populate(objectMap, "resourceType", r.ResourceType)
return json.Marshal(objectMap)
}
@@ -3062,12 +3062,12 @@ func (r *RecipeGetMetadata) UnmarshalJSON(data []byte) error {
for key, val := range rawMsg {
var err error
switch key {
- case "linkType":
- err = unpopulate(val, "LinkType", &r.LinkType)
- delete(rawMsg, key)
case "name":
err = unpopulate(val, "Name", &r.Name)
delete(rawMsg, key)
+ case "resourceType":
+ err = unpopulate(val, "ResourceType", &r.ResourceType)
+ delete(rawMsg, key)
}
if err != nil {
return fmt.Errorf("unmarshalling type %T: %v", r, err)
diff --git a/pkg/corerp/backend/deployment/deploymentprocessor_test.go b/pkg/corerp/backend/deployment/deploymentprocessor_test.go
index a0b5efac37..15f34ee043 100644
--- a/pkg/corerp/backend/deployment/deploymentprocessor_test.go
+++ b/pkg/corerp/backend/deployment/deploymentprocessor_test.go
@@ -175,10 +175,10 @@ func buildMongoDBWithRecipe() dsrp_dm.MongoDatabase {
Environment: "/subscriptions/test-subscription/resourceGroups/test-resource-group/providers/Applications.Core/environments/env0",
},
},
- LinkMetadata: pr_dm.LinkMetadata{
+ PortableResourceMetadata: pr_dm.PortableResourceMetadata{
RecipeData: portableresources.RecipeData{
RecipeProperties: portableresources.RecipeProperties{
- LinkRecipe: portableresources.LinkRecipe{
+ ResourceRecipe: portableresources.ResourceRecipe{
Name: "mongoDB",
Parameters: map[string]any{
"ResourceGroup": "testRG",
@@ -1043,7 +1043,7 @@ func Test_getResourceDataByID(t *testing.T) {
depId, _ := resources.ParseResource("/subscriptions/test-subscription/resourceGroups/test-resource-group/providers/Applications.Datastores/mongoDatabases/test-mongo")
mongoResource := buildMongoDBWithRecipe()
- mongoResource.LinkMetadata.RecipeData = portableresources.RecipeData{}
+ mongoResource.PortableResourceMetadata.RecipeData = portableresources.RecipeData{}
mr := store.Object{
Metadata: store.Metadata{
ID: mongoResource.ID,
diff --git a/pkg/corerp/datamodel/container.go b/pkg/corerp/datamodel/container.go
index 98a93c31a9..87fe8ec615 100644
--- a/pkg/corerp/datamodel/container.go
+++ b/pkg/corerp/datamodel/container.go
@@ -28,7 +28,7 @@ type ContainerResource struct {
v1.BaseResource
// TODO: remove this from CoreRP
- LinkMetadata
+ PortableResourceMetadata
// Properties is the properties of the resource.
Properties ContainerProperties `json:"properties"`
diff --git a/pkg/corerp/datamodel/environment.go b/pkg/corerp/datamodel/environment.go
index c3648f4d11..da36c496b4 100644
--- a/pkg/corerp/datamodel/environment.go
+++ b/pkg/corerp/datamodel/environment.go
@@ -55,7 +55,7 @@ type EnvironmentRecipeProperties struct {
// Recipe represents input properties for recipe getMetadata api.
type Recipe struct {
// Type of the portable resource this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'
- LinkType string `json:"linkType,omitempty"`
+ ResourceType string `json:"resourceType,omitempty"`
// Name of the recipe registered to the environment.
Name string `json:"recipeName,omitempty"`
diff --git a/pkg/corerp/datamodel/extender.go b/pkg/corerp/datamodel/extender.go
index a2b721d1bc..a0780546a3 100644
--- a/pkg/corerp/datamodel/extender.go
+++ b/pkg/corerp/datamodel/extender.go
@@ -32,8 +32,8 @@ type Extender struct {
// Properties is the properties of the resource.
Properties ExtenderProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable resource types.
- LinkMetadata
+ // PortableResourceMetadata represents internal DataModel properties common to all portable resource types.
+ PortableResourceMetadata
}
// ApplyDeploymentOutput updates the Status of Properties of the Extender resource with the DeployedOutputResources and returns no error.
@@ -57,9 +57,9 @@ func (extender *Extender) ResourceTypeName() string {
return ExtenderResourceType
}
-// Recipe returns the LinkRecipe associated with the Extender if the ResourceProvisioning is not set to Manual,
+// Recipe returns the ResourceRecipe associated with the Extender if the ResourceProvisioning is not set to Manual,
// otherwise it returns nil.
-func (extender *Extender) Recipe() *portableresources.LinkRecipe {
+func (extender *Extender) Recipe() *portableresources.ResourceRecipe {
if extender.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
@@ -74,7 +74,7 @@ type ExtenderProperties struct {
// Secrets values provided for the resource
Secrets map[string]any `json:"secrets,omitempty"`
// The recipe used to automatically deploy underlying infrastructure for the Extender
- ResourceRecipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ ResourceRecipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
// Specifies how the underlying service/resource is provisioned and managed
ResourceProvisioning portableresources.ResourceProvisioning `json:"resourceProvisioning,omitempty"`
}
diff --git a/pkg/corerp/datamodel/gateway.go b/pkg/corerp/datamodel/gateway.go
index 68c9a9e682..052cf17dae 100644
--- a/pkg/corerp/datamodel/gateway.go
+++ b/pkg/corerp/datamodel/gateway.go
@@ -28,7 +28,7 @@ type Gateway struct {
v1.BaseResource
// TODO: remove this from CoreRP
- LinkMetadata
+ PortableResourceMetadata
// Properties is the properties of the resource.
Properties GatewayProperties `json:"properties"`
}
diff --git a/pkg/corerp/datamodel/httproute.go b/pkg/corerp/datamodel/httproute.go
index bd59039180..7cfa894b37 100644
--- a/pkg/corerp/datamodel/httproute.go
+++ b/pkg/corerp/datamodel/httproute.go
@@ -28,7 +28,7 @@ type HTTPRoute struct {
v1.BaseResource
// TODO: remove this from CoreRP
- LinkMetadata
+ PortableResourceMetadata
// Properties is the properties of the resource.
Properties *HTTPRouteProperties `json:"properties"`
}
diff --git a/pkg/corerp/datamodel/linkmetadata.go b/pkg/corerp/datamodel/portableresourcemetadata.go
similarity index 88%
rename from pkg/corerp/datamodel/linkmetadata.go
rename to pkg/corerp/datamodel/portableresourcemetadata.go
index dd6b47fa47..e7bf3ffbee 100644
--- a/pkg/corerp/datamodel/linkmetadata.go
+++ b/pkg/corerp/datamodel/portableresourcemetadata.go
@@ -20,8 +20,8 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
-// LinkMetadata represents internal DataModel properties common to all portable resource types.
-type LinkMetadata struct {
+// PortableResourceMetadata represents internal DataModel properties common to all portable resource types.
+type PortableResourceMetadata struct {
// TODO: stop using this type in CoreRP models.
// ComputedValues map is any resource values that will be needed for more operations.
diff --git a/pkg/corerp/datamodel/secretstore.go b/pkg/corerp/datamodel/secretstore.go
index d7cbd55af2..5ec0993538 100644
--- a/pkg/corerp/datamodel/secretstore.go
+++ b/pkg/corerp/datamodel/secretstore.go
@@ -52,7 +52,7 @@ type SecretStore struct {
v1.BaseResource
// TODO: remove this from CoreRP
- LinkMetadata
+ PortableResourceMetadata
// Properties is the properties of the resource.
Properties *SecretStoreProperties `json:"properties"`
}
diff --git a/pkg/corerp/datamodel/volume.go b/pkg/corerp/datamodel/volume.go
index 924ff428fb..e7aee0521b 100644
--- a/pkg/corerp/datamodel/volume.go
+++ b/pkg/corerp/datamodel/volume.go
@@ -33,7 +33,7 @@ type VolumeResource struct {
v1.BaseResource
// TODO: remove this from CoreRP
- LinkMetadata
+ PortableResourceMetadata
// Properties is the properties of the resource.
Properties VolumeResourceProperties `json:"properties"`
diff --git a/pkg/corerp/frontend/controller/environments/getrecipemetadata.go b/pkg/corerp/frontend/controller/environments/getrecipemetadata.go
index 0511bc32ff..dacf58a561 100644
--- a/pkg/corerp/frontend/controller/environments/getrecipemetadata.go
+++ b/pkg/corerp/frontend/controller/environments/getrecipemetadata.go
@@ -74,12 +74,12 @@ func (r *GetRecipeMetadata) Run(ctx context.Context, w http.ResponseWriter, req
return nil, err
}
var recipeProperties datamodel.EnvironmentRecipeProperties
- recipe, exists := resource.Properties.Recipes[recipeDatamodel.LinkType]
+ recipe, exists := resource.Properties.Recipes[recipeDatamodel.ResourceType]
if exists {
recipeProperties, exists = recipe[recipeDatamodel.Name]
}
if !exists {
- return rest.NewNotFoundMessageResponse(fmt.Sprintf("Either recipe with name %q or resource type %q not found on environment with id %q", recipeDatamodel.Name, recipeDatamodel.LinkType, serviceCtx.ResourceID)), nil
+ return rest.NewNotFoundMessageResponse(fmt.Sprintf("Either recipe with name %q or resource type %q not found on environment with id %q", recipeDatamodel.Name, recipeDatamodel.ResourceType, serviceCtx.ResourceID)), nil
}
recipeParams, err := r.GetRecipeMetadataFromRegistry(ctx, recipeProperties, recipeDatamodel)
@@ -108,7 +108,7 @@ func (r *GetRecipeMetadata) GetRecipeMetadataFromRegistry(ctx context.Context, r
Parameters: recipeProperties.Parameters,
TemplatePath: recipeProperties.TemplatePath,
TemplateVersion: recipeProperties.TemplateVersion,
- ResourceType: recipeDataModel.LinkType,
+ ResourceType: recipeDataModel.ResourceType,
}
recipeParameters = make(map[string]any)
diff --git a/pkg/corerp/frontend/controller/environments/getrecipemetadata_test.go b/pkg/corerp/frontend/controller/environments/getrecipemetadata_test.go
index 35bbe040f0..ce1c3259e3 100644
--- a/pkg/corerp/frontend/controller/environments/getrecipemetadata_test.go
+++ b/pkg/corerp/frontend/controller/environments/getrecipemetadata_test.go
@@ -65,7 +65,7 @@ func TestGetRecipeMetadataRun_20220315PrivatePreview(t *testing.T) {
TemplatePath: "radiusdev.azurecr.io/recipes/functionaltest/parameters/mongodatabases/azure:1.0",
TemplateVersion: "",
Driver: "bicep",
- ResourceType: *envInput.LinkType,
+ ResourceType: *envInput.ResourceType,
}
recipeData := map[string]any{
"parameters": map[string]any{
@@ -113,7 +113,7 @@ func TestGetRecipeMetadataRun_20220315PrivatePreview(t *testing.T) {
TemplatePath: "Azure/cosmosdb/azurerm",
TemplateVersion: "1.1.0",
Driver: "terraform",
- ResourceType: *envInput.LinkType,
+ ResourceType: *envInput.ResourceType,
}
recipeData := map[string]any{
"parameters": map[string]any{
@@ -237,7 +237,7 @@ func TestGetRecipeMetadataRun_20220315PrivatePreview(t *testing.T) {
TemplatePath: "radiusdev.azurecr.io/recipes/functionaltest/parameters/mongodatabases/azure:1.0",
TemplateVersion: "",
Driver: "bicep",
- ResourceType: *envInput.LinkType,
+ ResourceType: *envInput.ResourceType,
}
engineErr := fmt.Errorf("could not find driver %s", "invalidDriver")
mEngine.EXPECT().GetRecipeMetadata(ctx, recipeDefinition).Return(nil, engineErr)
diff --git a/pkg/corerp/frontend/controller/environments/testdata/environmentgetmetadatanonexistingrecipe20220315privatepreview_input.json b/pkg/corerp/frontend/controller/environments/testdata/environmentgetmetadatanonexistingrecipe20220315privatepreview_input.json
index 5eb30a4ebe..f059c65f10 100644
--- a/pkg/corerp/frontend/controller/environments/testdata/environmentgetmetadatanonexistingrecipe20220315privatepreview_input.json
+++ b/pkg/corerp/frontend/controller/environments/testdata/environmentgetmetadatanonexistingrecipe20220315privatepreview_input.json
@@ -1,4 +1,4 @@
{
"name":"mongodb",
- "linkType":"Applications.Datastores/mongoDatabases"
+ "resourceType":"Applications.Datastores/mongoDatabases"
}
\ No newline at end of file
diff --git a/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input.json b/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input.json
index ab09f45543..7b16bdeca8 100644
--- a/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input.json
+++ b/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input.json
@@ -1,4 +1,4 @@
{
"name":"mongo-parameters",
- "linkType":"Applications.Datastores/mongoDatabases"
+ "resourceType":"Applications.Datastores/mongoDatabases"
}
\ No newline at end of file
diff --git a/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input_terraform.json b/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input_terraform.json
index 1331658ada..5939a0fc83 100644
--- a/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input_terraform.json
+++ b/pkg/corerp/frontend/controller/environments/testdata/environmentgetrecipemetadata20220315privatepreview_input_terraform.json
@@ -1,4 +1,4 @@
{
"name":"mongo-terraform",
- "linkType":"Applications.Datastores/mongoDatabases"
+ "resourceType":"Applications.Datastores/mongoDatabases"
}
\ No newline at end of file
diff --git a/pkg/daprrp/api/v20220315privatepreview/datamodel_util.go b/pkg/daprrp/api/v20220315privatepreview/datamodel_util.go
index 5ee5f719d1..fe1e1724b3 100644
--- a/pkg/daprrp/api/v20220315privatepreview/datamodel_util.go
+++ b/pkg/daprrp/api/v20220315privatepreview/datamodel_util.go
@@ -109,13 +109,13 @@ func fromSystemDataModel(s v1.SystemData) *SystemData {
}
}
-func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
+func toRecipeDataModel(r *Recipe) portableresources.ResourceRecipe {
if r == nil {
- return portableresources.LinkRecipe{
+ return portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
}
}
- recipe := portableresources.LinkRecipe{}
+ recipe := portableresources.ResourceRecipe{}
if r.Name == nil {
recipe.Name = portableresources.DefaultRecipeName
} else {
@@ -127,7 +127,7 @@ func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
return recipe
}
-func fromRecipeDataModel(r portableresources.LinkRecipe) *Recipe {
+func fromRecipeDataModel(r portableresources.ResourceRecipe) *Recipe {
return &Recipe{
Name: to.Ptr(r.Name),
Parameters: r.Parameters,
diff --git a/pkg/daprrp/api/v20220315privatepreview/datamodel_util_test.go b/pkg/daprrp/api/v20220315privatepreview/datamodel_util_test.go
index 77608de243..515e76ec06 100644
--- a/pkg/daprrp/api/v20220315privatepreview/datamodel_util_test.go
+++ b/pkg/daprrp/api/v20220315privatepreview/datamodel_util_test.go
@@ -215,11 +215,11 @@ func TestFromResourceProvisiongDataModel(t *testing.T) {
func TestToRecipeDataModel(t *testing.T) {
testset := []struct {
versioned *Recipe
- datamodel portableresources.LinkRecipe
+ datamodel portableresources.ResourceRecipe
}{
{
nil,
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
},
},
@@ -230,7 +230,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: "test",
Parameters: map[string]any{
"foo": "bar",
@@ -243,7 +243,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/daprrp/api/v20220315privatepreview/pubsubbroker_conversion_test.go b/pkg/daprrp/api/v20220315privatepreview/pubsubbroker_conversion_test.go
index 3a40262630..bddbe94961 100644
--- a/pkg/daprrp/api/v20220315privatepreview/pubsubbroker_conversion_test.go
+++ b/pkg/daprrp/api/v20220315privatepreview/pubsubbroker_conversion_test.go
@@ -103,7 +103,7 @@ func TestDaprPubSubBroker_ConvertVersionedToDataModel(t *testing.T) {
Environment: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/test-env",
},
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
- Recipe: portableresources.LinkRecipe{
+ Recipe: portableresources.ResourceRecipe{
Name: "dpsb-recipe",
},
},
diff --git a/pkg/daprrp/api/v20220315privatepreview/secretstore_conversion_test.go b/pkg/daprrp/api/v20220315privatepreview/secretstore_conversion_test.go
index 7a94d9e5bc..8d47ae1e83 100644
--- a/pkg/daprrp/api/v20220315privatepreview/secretstore_conversion_test.go
+++ b/pkg/daprrp/api/v20220315privatepreview/secretstore_conversion_test.go
@@ -98,7 +98,7 @@ func TestDaprSecretStore_ConvertVersionedToDataModel(t *testing.T) {
Environment: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/test-env",
},
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
- Recipe: portableresources.LinkRecipe{
+ Recipe: portableresources.ResourceRecipe{
Name: "daprSecretStore",
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/daprrp/datamodel/daprpubsubbroker.go b/pkg/daprrp/datamodel/daprpubsubbroker.go
index 94b2553b21..35367d6331 100644
--- a/pkg/daprrp/datamodel/daprpubsubbroker.go
+++ b/pkg/daprrp/datamodel/daprpubsubbroker.go
@@ -30,8 +30,8 @@ type DaprPubSubBroker struct {
// Properties is the properties of the resource.
Properties DaprPubSubBrokerProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable resource types.
- pr_dm.LinkMetadata
+ // ResourceMetadata represents internal DataModel properties common to all portable resource types.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput updates the DaprPubSubBroker resource with the DeploymentOutput values.
@@ -55,7 +55,7 @@ func (daprPubSub *DaprPubSubBroker) ResourceTypeName() string {
}
// Recipe returns the recipe information of the resource. Returns nil if recipe execution is disabled.
-func (r *DaprPubSubBroker) Recipe() *portableresources.LinkRecipe {
+func (r *DaprPubSubBroker) Recipe() *portableresources.ResourceRecipe {
if r.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
@@ -74,7 +74,7 @@ type DaprPubSubBrokerProperties struct {
Metadata map[string]any `json:"metadata,omitempty"`
// The recipe used to automatically deploy underlying infrastructure for the Dapr Pub/Sub Broker resource.
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
// List of the resource IDs that support the Dapr Pub/Sub Broker resource.
Resources []*portableresources.ResourceReference `json:"resources,omitempty"`
diff --git a/pkg/daprrp/datamodel/daprsecretstore.go b/pkg/daprrp/datamodel/daprsecretstore.go
index 41bfd2fc51..d6bdad5e5e 100644
--- a/pkg/daprrp/datamodel/daprsecretstore.go
+++ b/pkg/daprrp/datamodel/daprsecretstore.go
@@ -30,8 +30,8 @@ type DaprSecretStore struct {
// Properties is the properties of the resource.
Properties DaprSecretStoreProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable resource types.
- pr_dm.LinkMetadata
+ // ResourceMetadata represents internal DataModel properties common to all portable resource types.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput updates the DaprSecretStore resource with the DeploymentOutput values.
@@ -61,13 +61,13 @@ type DaprSecretStoreProperties struct {
Type string `json:"type,omitempty"`
Version string `json:"version,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
ResourceProvisioning portableresources.ResourceProvisioning `json:"resourceProvisioning,omitempty"`
}
// Recipe returns the Recipe from the DaprSecretStore Properties if ResourceProvisioning is not set to Manual,
// otherwise it returns nil.
-func (daprSecretStore *DaprSecretStore) Recipe() *portableresources.LinkRecipe {
+func (daprSecretStore *DaprSecretStore) Recipe() *portableresources.ResourceRecipe {
if daprSecretStore.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
diff --git a/pkg/daprrp/datamodel/daprstatestore.go b/pkg/daprrp/datamodel/daprstatestore.go
index 6c2efc5869..fde21de75c 100644
--- a/pkg/daprrp/datamodel/daprstatestore.go
+++ b/pkg/daprrp/datamodel/daprstatestore.go
@@ -30,8 +30,8 @@ type DaprStateStore struct {
// Properties is the properties of the resource.
Properties DaprStateStoreProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable types.
- pr_dm.LinkMetadata
+ // PortableResourceMetadata represents internal DataModel properties common to all portable types.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput updates the DaprStateStore resource with the DeploymentOutput values.
@@ -55,7 +55,7 @@ func (daprStateStore *DaprStateStore) ResourceTypeName() string {
}
// Recipe returns the recipe information of the resource. It returns nil if the ResourceProvisioning is set to manual.
-func (r *DaprStateStore) Recipe() *portableresources.LinkRecipe {
+func (r *DaprStateStore) Recipe() *portableresources.ResourceRecipe {
if r.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
@@ -69,7 +69,7 @@ type DaprStateStoreProperties struct {
// Specifies how the underlying service/resource is provisioned and managed
ResourceProvisioning portableresources.ResourceProvisioning `json:"resourceProvisioning,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
Resources []*portableresources.ResourceReference `json:"resources,omitempty"`
Type string `json:"type,omitempty"`
Version string `json:"version,omitempty"`
diff --git a/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util.go b/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util.go
index 823be224ec..64cd7d4158 100644
--- a/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util.go
+++ b/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util.go
@@ -98,13 +98,13 @@ func fromResourceProvisioningDataModel(provisioning portableresources.ResourcePr
return &converted
}
-func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
+func toRecipeDataModel(r *Recipe) portableresources.ResourceRecipe {
if r == nil {
- return portableresources.LinkRecipe{
+ return portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
}
}
- recipe := portableresources.LinkRecipe{}
+ recipe := portableresources.ResourceRecipe{}
if r.Name == nil {
recipe.Name = portableresources.DefaultRecipeName
} else {
@@ -116,7 +116,7 @@ func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
return recipe
}
-func fromRecipeDataModel(r portableresources.LinkRecipe) *Recipe {
+func fromRecipeDataModel(r portableresources.ResourceRecipe) *Recipe {
return &Recipe{
Name: to.Ptr(r.Name),
Parameters: r.Parameters,
diff --git a/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util_test.go b/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util_test.go
index b7bb9a2897..1162b6aeb2 100644
--- a/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util_test.go
+++ b/pkg/datastoresrp/api/v20220315privatepreview/datamodel_util_test.go
@@ -214,11 +214,11 @@ func TestFromResourceProvisiongDataModel(t *testing.T) {
func TestToRecipeDataModel(t *testing.T) {
testset := []struct {
versioned *Recipe
- datamodel portableresources.LinkRecipe
+ datamodel portableresources.ResourceRecipe
}{
{
nil,
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
},
},
@@ -229,7 +229,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: "test",
Parameters: map[string]any{
"foo": "bar",
@@ -242,7 +242,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
Parameters: map[string]any{
"foo": "bar",
@@ -258,11 +258,11 @@ func TestToRecipeDataModel(t *testing.T) {
func TestFromRecipeDataModel(t *testing.T) {
testset := []struct {
- DMResources []portableresources.LinkRecipe
+ DMResources []portableresources.ResourceRecipe
VersionedResources []*Recipe
}{
{
- DMResources: []portableresources.LinkRecipe{{
+ DMResources: []portableresources.ResourceRecipe{{
Name: portableresources.DefaultRecipeName,
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/datastoresrp/api/v20220315privatepreview/mongodatabase_conversion_test.go b/pkg/datastoresrp/api/v20220315privatepreview/mongodatabase_conversion_test.go
index 5b7866472e..a0dd6af485 100644
--- a/pkg/datastoresrp/api/v20220315privatepreview/mongodatabase_conversion_test.go
+++ b/pkg/datastoresrp/api/v20220315privatepreview/mongodatabase_conversion_test.go
@@ -94,7 +94,7 @@ func TestMongoDatabase_ConvertVersionedToDataModel(t *testing.T) {
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
Host: "testAccount.mongo.cosmos.azure.com",
Port: 10255,
- Recipe: portableresources.LinkRecipe{Name: "cosmosdb", Parameters: map[string]interface{}{"foo": "bar"}},
+ Recipe: portableresources.ResourceRecipe{Name: "cosmosdb", Parameters: map[string]interface{}{"foo": "bar"}},
},
},
},
@@ -125,7 +125,7 @@ func TestMongoDatabase_ConvertVersionedToDataModel(t *testing.T) {
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
Host: "mynewhost.com",
Port: 10256,
- Recipe: portableresources.LinkRecipe{Name: portableresources.DefaultRecipeName, Parameters: nil},
+ Recipe: portableresources.ResourceRecipe{Name: portableresources.DefaultRecipeName, Parameters: nil},
},
},
},
diff --git a/pkg/datastoresrp/api/v20220315privatepreview/rediscache_conversion_test.go b/pkg/datastoresrp/api/v20220315privatepreview/rediscache_conversion_test.go
index 863c72de59..cbf576231c 100644
--- a/pkg/datastoresrp/api/v20220315privatepreview/rediscache_conversion_test.go
+++ b/pkg/datastoresrp/api/v20220315privatepreview/rediscache_conversion_test.go
@@ -56,7 +56,7 @@ func TestRedisCache_ConvertVersionedToDataModel(t *testing.T) {
Port: 0,
TLS: false,
Username: "",
- Recipe: portableresources.LinkRecipe{Name: "default"},
+ Recipe: portableresources.ResourceRecipe{Name: "default"},
},
},
},
@@ -72,7 +72,7 @@ func TestRedisCache_ConvertVersionedToDataModel(t *testing.T) {
Port: 0,
TLS: false,
Username: "",
- Recipe: portableresources.LinkRecipe{Name: "redis-test"},
+ Recipe: portableresources.ResourceRecipe{Name: "redis-test"},
},
},
},
@@ -88,7 +88,7 @@ func TestRedisCache_ConvertVersionedToDataModel(t *testing.T) {
Port: 10255,
TLS: false,
Username: "",
- Recipe: portableresources.LinkRecipe{Name: "redis-test", Parameters: map[string]any{"port": float64(6081)}},
+ Recipe: portableresources.ResourceRecipe{Name: "redis-test", Parameters: map[string]any{"port": float64(6081)}},
},
},
},
diff --git a/pkg/datastoresrp/api/v20220315privatepreview/sqldatabase_conversion_test.go b/pkg/datastoresrp/api/v20220315privatepreview/sqldatabase_conversion_test.go
index 05c71ed1c8..ef60a87d85 100644
--- a/pkg/datastoresrp/api/v20220315privatepreview/sqldatabase_conversion_test.go
+++ b/pkg/datastoresrp/api/v20220315privatepreview/sqldatabase_conversion_test.go
@@ -106,7 +106,7 @@ func TestSqlDatabase_ConvertVersionedToDataModel(t *testing.T) {
Environment: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/test-env",
},
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
- Recipe: portableresources.LinkRecipe{
+ Recipe: portableresources.ResourceRecipe{
Name: "sql-test",
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/datastoresrp/datamodel/mongodatabase.go b/pkg/datastoresrp/datamodel/mongodatabase.go
index 3882d0ab37..322996f187 100644
--- a/pkg/datastoresrp/datamodel/mongodatabase.go
+++ b/pkg/datastoresrp/datamodel/mongodatabase.go
@@ -30,8 +30,8 @@ import (
type MongoDatabase struct {
v1.BaseResource
- // LinkMetadata represents internal DataModel properties common to all portable resources.
- pr_dm.LinkMetadata
+ // PortableResourceMetadata represents internal DataModel properties common to all portable resources.
+ pr_dm.PortableResourceMetadata
// Properties is the properties of the resource.
Properties MongoDatabaseProperties `json:"properties"`
@@ -49,7 +49,7 @@ type MongoDatabaseProperties struct {
// Database name of the target Mongo database
Database string `json:"database,omitempty"`
// The recipe used to automatically deploy underlying infrastructure for the Mongo database link
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
// List of the resource IDs that support the Mongo database resource
Resources []*portableresources.ResourceReference `json:"resources,omitempty"`
// Specifies how the underlying service/resource is provisioned and managed
@@ -115,9 +115,9 @@ func (r *MongoDatabase) ResourceMetadata() *rpv1.BasicResourceProperties {
return &r.Properties.BasicResourceProperties
}
-// Recipe returns the LinkRecipe associated with the Mongo database instance, or nil if the
+// Recipe returns the ResourceRecipe associated with the Mongo database instance, or nil if the
// ResourceProvisioning is set to Manual.
-func (r *MongoDatabase) Recipe() *portableresources.LinkRecipe {
+func (r *MongoDatabase) Recipe() *portableresources.ResourceRecipe {
if r.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
diff --git a/pkg/datastoresrp/datamodel/rediscache.go b/pkg/datastoresrp/datamodel/rediscache.go
index 1b9db8f07e..67790d7d06 100644
--- a/pkg/datastoresrp/datamodel/rediscache.go
+++ b/pkg/datastoresrp/datamodel/rediscache.go
@@ -33,8 +33,8 @@ type RedisCache struct {
// Properties is the properties of the resource.
Properties RedisCacheProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all link types.
- pr_dm.LinkMetadata
+ // PortableResourceMetadata represents internal DataModel properties common to all resource types.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput sets the Status, ComputedValues, SecretValues, Host, Port and Username properties of the
@@ -58,9 +58,9 @@ func (redis *RedisCache) ResourceTypeName() string {
return portableresources.RedisCachesResourceType
}
-// Recipe returns the LinkRecipe from the Redis cache Properties if ResourceProvisioning is not set to Manual,
+// Recipe returns the ResourceRecipe from the Redis cache Properties if ResourceProvisioning is not set to Manual,
// otherwise it returns nil.
-func (redis *RedisCache) Recipe() *portableresources.LinkRecipe {
+func (redis *RedisCache) Recipe() *portableresources.ResourceRecipe {
if redis.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
@@ -115,7 +115,7 @@ type RedisCacheProperties struct {
TLS bool `json:"tls,omitempty"`
// The recipe used to automatically deploy underlying infrastructure for the Redis caches link
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
// Secrets provided by resource
Secrets RedisCacheSecrets `json:"secrets,omitempty"`
diff --git a/pkg/datastoresrp/datamodel/sqldatabase.go b/pkg/datastoresrp/datamodel/sqldatabase.go
index 1bf003ff86..3cfad887af 100644
--- a/pkg/datastoresrp/datamodel/sqldatabase.go
+++ b/pkg/datastoresrp/datamodel/sqldatabase.go
@@ -26,9 +26,9 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
-// Recipe returns the LinkRecipe associated with the SQL database instance if the ResourceProvisioning is not
+// Recipe returns the ResourceRecipe associated with the SQL database instance if the ResourceProvisioning is not
// set to Manual, otherwise it returns nil.
-func (sql *SqlDatabase) Recipe() *portableresources.LinkRecipe {
+func (sql *SqlDatabase) Recipe() *portableresources.ResourceRecipe {
if sql.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
@@ -42,8 +42,8 @@ type SqlDatabase struct {
// Properties is the properties of the resource.
Properties SqlDatabaseProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable resources.
- pr_dm.LinkMetadata
+ // ResourceMetadata represents internal DataModel properties common to all portable resources.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput updates the output resources of a SQL database resource with the output resources of a DeploymentOutput
@@ -71,7 +71,7 @@ func (sql *SqlDatabase) ResourceTypeName() string {
type SqlDatabaseProperties struct {
rpv1.BasicResourceProperties
// The recipe used to automatically deploy underlying infrastructure for the SQL database resource
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
// Database name of the target SQL database resource
Database string `json:"database,omitempty"`
// The fully qualified domain name of the SQL database resource
diff --git a/pkg/messagingrp/api/v20220315privatepreview/datamodel_util.go b/pkg/messagingrp/api/v20220315privatepreview/datamodel_util.go
index 5ee5f719d1..fe1e1724b3 100644
--- a/pkg/messagingrp/api/v20220315privatepreview/datamodel_util.go
+++ b/pkg/messagingrp/api/v20220315privatepreview/datamodel_util.go
@@ -109,13 +109,13 @@ func fromSystemDataModel(s v1.SystemData) *SystemData {
}
}
-func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
+func toRecipeDataModel(r *Recipe) portableresources.ResourceRecipe {
if r == nil {
- return portableresources.LinkRecipe{
+ return portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
}
}
- recipe := portableresources.LinkRecipe{}
+ recipe := portableresources.ResourceRecipe{}
if r.Name == nil {
recipe.Name = portableresources.DefaultRecipeName
} else {
@@ -127,7 +127,7 @@ func toRecipeDataModel(r *Recipe) portableresources.LinkRecipe {
return recipe
}
-func fromRecipeDataModel(r portableresources.LinkRecipe) *Recipe {
+func fromRecipeDataModel(r portableresources.ResourceRecipe) *Recipe {
return &Recipe{
Name: to.Ptr(r.Name),
Parameters: r.Parameters,
diff --git a/pkg/messagingrp/api/v20220315privatepreview/datamodel_util_test.go b/pkg/messagingrp/api/v20220315privatepreview/datamodel_util_test.go
index 4f886f97ef..e68b650625 100644
--- a/pkg/messagingrp/api/v20220315privatepreview/datamodel_util_test.go
+++ b/pkg/messagingrp/api/v20220315privatepreview/datamodel_util_test.go
@@ -116,11 +116,11 @@ func TestFromSystemDataModel(t *testing.T) {
func TestToRecipeDataModel(t *testing.T) {
testset := []struct {
versioned *Recipe
- datamodel portableresources.LinkRecipe
+ datamodel portableresources.ResourceRecipe
}{
{
nil,
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
},
},
@@ -131,7 +131,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: "test",
Parameters: map[string]any{
"foo": "bar",
@@ -144,7 +144,7 @@ func TestToRecipeDataModel(t *testing.T) {
"foo": "bar",
},
},
- portableresources.LinkRecipe{
+ portableresources.ResourceRecipe{
Name: portableresources.DefaultRecipeName,
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/messagingrp/api/v20220315privatepreview/rabbitmq_conversion_test.go b/pkg/messagingrp/api/v20220315privatepreview/rabbitmq_conversion_test.go
index 899020a827..408ab20133 100644
--- a/pkg/messagingrp/api/v20220315privatepreview/rabbitmq_conversion_test.go
+++ b/pkg/messagingrp/api/v20220315privatepreview/rabbitmq_conversion_test.go
@@ -104,7 +104,7 @@ func TestRabbitMQQueue_ConvertVersionedToDataModel(t *testing.T) {
},
ResourceProvisioning: portableresources.ResourceProvisioningRecipe,
TLS: false,
- Recipe: portableresources.LinkRecipe{
+ Recipe: portableresources.ResourceRecipe{
Name: "rabbitmq",
Parameters: map[string]any{
"foo": "bar",
diff --git a/pkg/messagingrp/datamodel/rabbitmq.go b/pkg/messagingrp/datamodel/rabbitmq.go
index a16b840a54..cbc8e833e9 100644
--- a/pkg/messagingrp/datamodel/rabbitmq.go
+++ b/pkg/messagingrp/datamodel/rabbitmq.go
@@ -33,8 +33,8 @@ type RabbitMQQueue struct {
// Properties is the properties of the resource.
Properties RabbitMQQueueProperties `json:"properties"`
- // LinkMetadata represents internal DataModel properties common to all portable resource types.
- pr_dm.LinkMetadata
+ // ResourceMetadata represents internal DataModel properties common to all portable resource types.
+ pr_dm.PortableResourceMetadata
}
// ApplyDeploymentOutput updates the RabbitMQQueue instance with the DeployedOutputResources from the
@@ -67,7 +67,7 @@ type RabbitMQQueueProperties struct {
VHost string `json:"vHost,omitempty"`
Username string `json:"username,omitempty"`
Resources []*portableresources.ResourceReference `json:"resources,omitempty"`
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
Secrets RabbitMQSecrets `json:"secrets,omitempty"`
ResourceProvisioning portableresources.ResourceProvisioning `json:"resourceProvisioning,omitempty"`
TLS bool `json:"tls,omitempty"`
@@ -84,9 +84,9 @@ func (rabbitmq RabbitMQSecrets) ResourceTypeName() string {
return portableresources.RabbitMQQueuesResourceType
}
-// Recipe returns the recipe for the RabbitMQQueue. It gets the LinkRecipe associated with the RabbitMQQueue instance
+// Recipe returns the recipe for the RabbitMQQueue. It gets the ResourceRecipe associated with the RabbitMQQueue instance
// if the ResourceProvisioning is not set to Manual, otherwise it returns nil.
-func (r *RabbitMQQueue) Recipe() *portableresources.LinkRecipe {
+func (r *RabbitMQQueue) Recipe() *portableresources.ResourceRecipe {
if r.Properties.ResourceProvisioning == portableresources.ResourceProvisioningManual {
return nil
}
diff --git a/pkg/portableresources/backend/controller/createorupdateresource_test.go b/pkg/portableresources/backend/controller/createorupdateresource_test.go
index f47a801e44..188bca0f75 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource_test.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource_test.go
@@ -49,8 +49,8 @@ const (
type TestResource struct {
v1.BaseResource
- // LinkMetadata represents internal DataModel properties common to all portable resource types.
- datamodel.LinkMetadata
+ // ResourceMetadata represents internal DataModel properties common to all portable resource types.
+ datamodel.PortableResourceMetadata
// Properties is the properties of the resource.
Properties TestResourceProperties `json:"properties"`
@@ -74,15 +74,15 @@ func (r *TestResource) ResourceMetadata() *rpv1.BasicResourceProperties {
return &r.Properties.BasicResourceProperties
}
-// Recipe returns a pointer to the LinkRecipe stored in the Properties field of the TestResource struct.
-func (t *TestResource) Recipe() *portableresources.LinkRecipe {
+// Recipe returns a pointer to the ResourceRecipe stored in the Properties field of the TestResource struct.
+func (t *TestResource) Recipe() *portableresources.ResourceRecipe {
return &t.Properties.Recipe
}
type TestResourceProperties struct {
rpv1.BasicResourceProperties
- IsProcessed bool `json:"isProcessed"`
- Recipe portableresources.LinkRecipe `json:"recipe,omitempty"`
+ IsProcessed bool `json:"isProcessed"`
+ Recipe portableresources.ResourceRecipe `json:"recipe,omitempty"`
}
type SuccessProcessor struct {
diff --git a/pkg/portableresources/datamodel/linkmetadata.go b/pkg/portableresources/datamodel/metadata.go
similarity index 66%
rename from pkg/portableresources/datamodel/linkmetadata.go
rename to pkg/portableresources/datamodel/metadata.go
index 1282dd0f0e..c506915c29 100644
--- a/pkg/portableresources/datamodel/linkmetadata.go
+++ b/pkg/portableresources/datamodel/metadata.go
@@ -21,8 +21,13 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
)
-// LinkMetadata represents internal DataModel properties common to all portable resource types.
-type LinkMetadata struct {
+const (
+ // RecipeContextParameter is the parameter context for recipe deployment
+ RecipeContextParameter string = "context"
+)
+
+// PortableResourceMetadata represents internal DataModel properties common to all portable resource types.
+type PortableResourceMetadata struct {
// ComputedValues map is any resource values that will be needed for more operations.
// For example; database name to generate secrets for cosmos DB.
ComputedValues map[string]any `json:"computedValues,omitempty"`
@@ -32,17 +37,3 @@ type LinkMetadata struct {
RecipeData portableresources.RecipeData `json:"recipeData,omitempty"`
}
-
-// LinkMode specifies how to build a portable resource. Options are to build automatically via โrecipeโ or โresourceโ, or build manually via โvaluesโ. Selection determines which set of fields to additionally require.
-type LinkMode string
-
-const (
- // LinkModeRecipe is the recipe mode for portable resource deployment
- LinkModeRecipe LinkMode = "recipe"
- // LinkModeResource is the resource mode for portable resource deployment
- LinkModeResource LinkMode = "resource"
- // LinkModeResource is the values mode for portable resource deployment
- LinkModeValues LinkMode = "values"
- // RecipeContextParameter is the parameter context for recipe deployment
- RecipeContextParameter string = "context"
-)
diff --git a/pkg/portableresources/datamodel/recipes.go b/pkg/portableresources/datamodel/recipes.go
index 6bf152a331..a39218fa80 100644
--- a/pkg/portableresources/datamodel/recipes.go
+++ b/pkg/portableresources/datamodel/recipes.go
@@ -27,5 +27,5 @@ import (
// RecipeDataModel should be implemented on the datamodel of types that support recipes.
type RecipeDataModel interface {
// Recipe provides access to the user-specified recipe configuration. Can return nil.
- Recipe() *portableresources.LinkRecipe
+ Recipe() *portableresources.ResourceRecipe
}
diff --git a/pkg/portableresources/types.go b/pkg/portableresources/types.go
index 68ef24ec90..c77c2c268c 100644
--- a/pkg/portableresources/types.go
+++ b/pkg/portableresources/types.go
@@ -58,7 +58,7 @@ const (
type RecipeData struct {
RecipeProperties
- // APIVersion is the API version to use to perform operations on resources supported by the link.
+ // APIVersion is the API version to use to perform operations on resources.
// For example for Azure resources, every service has different REST API version that must be specified in the request.
APIVersion string
@@ -68,14 +68,14 @@ type RecipeData struct {
// RecipeProperties represents the information needed to deploy a recipe
type RecipeProperties struct {
- LinkRecipe // LinkRecipe is the recipe of the resource to be deployed
- LinkType string // LinkType represent the type of the link
- TemplatePath string // TemplatePath represent the recipe location
- EnvParameters map[string]any // EnvParameters represents the parameters set by the operator while linking the recipe to an environment
+ ResourceRecipe // ResourceRecipe is the recipe of the resource to be deployed
+ ResourceType string // ResourceType represent the type of the resource
+ TemplatePath string // TemplatePath represent the recipe location
+ EnvParameters map[string]any // EnvParameters represents the parameters set by the operator while linking the recipe to an environment
}
-// LinkRecipe is the recipe details used to automatically deploy underlying infrastructure for a link
-type LinkRecipe struct {
+// ResourceRecipe is the recipe details used to automatically deploy underlying infrastructure for a resource.
+type ResourceRecipe struct {
// Name of the recipe within the environment to use
Name string `json:"name,omitempty"`
// Parameters are key/value parameters to pass into the recipe at deployment
@@ -100,7 +100,7 @@ type RecipeContext struct {
}
// Resource contains the information needed to deploy a recipe.
-// In the case the resource is a Link, it represents the Link's id, name and type.
+// In the case the resource is a portable resource, it represents the resource's id, name and type.
type Resource struct {
ResourceInfo
Type string `json:"type"`
diff --git a/pkg/recipes/recipecontext/context.go b/pkg/recipes/recipecontext/context.go
index 0366a4728c..2e9fdea37e 100644
--- a/pkg/recipes/recipecontext/context.go
+++ b/pkg/recipes/recipecontext/context.go
@@ -30,7 +30,7 @@ var (
ErrParseFormat = "failed to parse %s: %q while building the recipe context parameter %w"
)
-// New creates the context parameter for the recipe with the link, environment and application info
+// New creates the context parameter for the recipe with the portable resource, environment, and application info
func New(metadata *recipes.ResourceMetadata, config *recipes.Configuration) (*Context, error) {
parsedResource, err := resources.ParseResource(metadata.ResourceID)
if err != nil {
diff --git a/pkg/recipes/recipecontext/types.go b/pkg/recipes/recipecontext/types.go
index 20e11a9f8b..f905fbcb89 100644
--- a/pkg/recipes/recipecontext/types.go
+++ b/pkg/recipes/recipecontext/types.go
@@ -44,7 +44,7 @@ type Context struct {
}
// Resource contains the information needed to deploy a recipe.
-// In the case the resource is a Link, it represents the Link's id, name and type.
+// In the case the resource is a portable resource, it represents the resource's id, name and type.
type Resource struct {
// ResourceInfo represents name and id of the resource
ResourceInfo
diff --git a/pkg/rp/doc.go b/pkg/rp/doc.go
index 3cd006fe3c..9caafd8a16 100644
--- a/pkg/rp/doc.go
+++ b/pkg/rp/doc.go
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-// rp package includes common packages which are shared by corerp and linkrp.
+// rp package includes common packages which are shared by corerp and portable resources rps (datastoresrp, messagingrp, daprrp).
package rp
diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/examples/Environments_GetRecipeMetadata.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/examples/Environments_GetRecipeMetadata.json
index 1e5300771d..d17ab65f66 100644
--- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/examples/Environments_GetRecipeMetadata.json
+++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/examples/Environments_GetRecipeMetadata.json
@@ -10,7 +10,7 @@
"responses": {
"200": {
"body": {
- "linkType": "Applications.Datastores/mongoDatabases",
+ "resourceType": "Applications.Datastores/mongoDatabases",
"templateKind": "bicep",
"templatePath": "br:sampleregistry.azureacr.io/radius/recipes/cosmosdb",
"parameters": {
diff --git a/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/openapi.json b/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/openapi.json
index d006909cbb..2cb9c48775 100644
--- a/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/openapi.json
+++ b/swagger/specification/applications/resource-manager/Applications.Core/preview/2022-03-15-privatepreview/openapi.json
@@ -4460,9 +4460,9 @@
"type": "object",
"description": "Represents the request body of the getmetadata action.",
"properties": {
- "linkType": {
+ "resourceType": {
"type": "string",
- "description": "Type of the link this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'"
+ "description": "Type of the resource this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'"
},
"name": {
"type": "string",
@@ -4470,7 +4470,7 @@
}
},
"required": [
- "linkType",
+ "resourceType",
"name"
]
},
diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_GetPlaneLocal.json b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_GetPlaneLocal.json
index de4215466c..ba3efe8c02 100644
--- a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_GetPlaneLocal.json
+++ b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_GetPlaneLocal.json
@@ -15,7 +15,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443"
},
"kind": "UCPNative"
diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_List.json b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_List.json
index 7a43a4d64e..2f22f7beb5 100644
--- a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_List.json
+++ b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_List.json
@@ -15,7 +15,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443",
"Microsoft.Resources": "http://bicep-de.radius-system:6443"
},
diff --git a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_ListPlanesByType.json b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_ListPlanesByType.json
index 3376f3b537..1544d77af2 100644
--- a/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_ListPlanesByType.json
+++ b/swagger/specification/ucp/resource-manager/UCP/preview/2022-09-01-privatepreview/examples/Planes_ListPlanesByType.json
@@ -16,7 +16,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443"
},
"kind": "UCPNative"
diff --git a/test/functional/shared/cli/cli_test.go b/test/functional/shared/cli/cli_test.go
index ac075f5ce5..a380184825 100644
--- a/test/functional/shared/cli/cli_test.go
+++ b/test/functional/shared/cli/cli_test.go
@@ -61,12 +61,12 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test shared.RPTest) {
recipeName := "recipeName"
recipeTemplate := "testpublicrecipe.azurecr.io/bicep/modules/testTemplate:v1"
templateKind := "bicep"
- linkType := "Applications.Datastores/mongoDatabases"
+ resourceType := "Applications.Datastores/mongoDatabases"
file := "testdata/corerp-redis-recipe.bicep"
target := fmt.Sprintf("br:radiusdev.azurecr.io/test-bicep-recipes/redis-recipe:%s", generateUniqueTag())
t.Run("Validate rad recipe register", func(t *testing.T) {
- output, err := cli.RecipeRegister(ctx, envName, recipeName, templateKind, recipeTemplate, linkType)
+ output, err := cli.RecipeRegister(ctx, envName, recipeName, templateKind, recipeTemplate, resourceType)
require.NoError(t, err)
require.Contains(t, output, "Successfully linked recipe")
})
@@ -75,12 +75,12 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test shared.RPTest) {
output, err := cli.RecipeList(ctx, envName)
require.NoError(t, err)
require.Regexp(t, recipeName, output)
- require.Regexp(t, linkType, output)
+ require.Regexp(t, resourceType, output)
require.Regexp(t, recipeTemplate, output)
})
t.Run("Validate rad recipe unregister", func(t *testing.T) {
- output, err := cli.RecipeUnregister(ctx, envName, recipeName, linkType)
+ output, err := cli.RecipeUnregister(ctx, envName, recipeName, resourceType)
require.NoError(t, err)
require.Contains(t, output, "Successfully unregistered recipe")
})
@@ -88,15 +88,15 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test shared.RPTest) {
t.Run("Validate rad recipe show", func(t *testing.T) {
showRecipeName := "mongodbtest"
showRecipeTemplate := "radiusdev.azurecr.io/recipes/functionaltest/parameters/mongodatabases/azure:1.0"
- showRecipeLinkType := "Applications.Datastores/mongoDatabases"
- output, err := cli.RecipeRegister(ctx, envName, showRecipeName, templateKind, showRecipeTemplate, showRecipeLinkType)
+ showRecipeResourceType := "Applications.Datastores/mongoDatabases"
+ output, err := cli.RecipeRegister(ctx, envName, showRecipeName, templateKind, showRecipeTemplate, showRecipeResourceType)
require.NoError(t, err)
require.Contains(t, output, "Successfully linked recipe")
- output, err = cli.RecipeShow(ctx, envName, showRecipeName, linkType)
+ output, err = cli.RecipeShow(ctx, envName, showRecipeName, resourceType)
require.NoError(t, err)
require.Contains(t, output, showRecipeName)
require.Contains(t, output, showRecipeTemplate)
- require.Contains(t, output, showRecipeLinkType)
+ require.Contains(t, output, showRecipeResourceType)
require.Contains(t, output, "mongodbName")
require.Contains(t, output, "documentdbName")
require.Contains(t, output, "location")
@@ -111,15 +111,15 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test shared.RPTest) {
moduleServer = "http://localhost:8999"
}
showRecipeTemplate := fmt.Sprintf("%s/kubernetes-redis.zip", moduleServer)
- showRecipeLinkType := "Applications.Datastores/redisCaches"
- output, err := cli.RecipeRegister(ctx, envName, showRecipeName, "terraform", showRecipeTemplate, showRecipeLinkType)
+ showRecipeResourceType := "Applications.Datastores/redisCaches"
+ output, err := cli.RecipeRegister(ctx, envName, showRecipeName, "terraform", showRecipeTemplate, showRecipeResourceType)
require.NoError(t, err)
require.Contains(t, output, "Successfully linked recipe")
- output, err = cli.RecipeShow(ctx, envName, showRecipeName, showRecipeLinkType)
+ output, err = cli.RecipeShow(ctx, envName, showRecipeName, showRecipeResourceType)
require.NoError(t, err)
require.Contains(t, output, showRecipeName)
require.Contains(t, output, showRecipeTemplate)
- require.Contains(t, output, showRecipeLinkType)
+ require.Contains(t, output, showRecipeResourceType)
require.Contains(t, output, "redis_cache_name")
require.Contains(t, output, "string")
})
@@ -131,7 +131,7 @@ func verifyRecipeCLI(ctx context.Context, t *testing.T, test shared.RPTest) {
})
t.Run("Validate rad recipe register with recipe name conflicting with dev recipe", func(t *testing.T) {
- output, err := cli.RecipeRegister(ctx, envName, "mongo-azure", templateKind, recipeTemplate, linkType)
+ output, err := cli.RecipeRegister(ctx, envName, "mongo-azure", templateKind, recipeTemplate, resourceType)
require.Contains(t, output, "Successfully linked recipe")
require.NoError(t, err)
output, err = cli.RecipeList(ctx, envName)
diff --git a/test/radcli/cli.go b/test/radcli/cli.go
index eb258a8d0f..4ba7989969 100644
--- a/test/radcli/cli.go
+++ b/test/radcli/cli.go
@@ -307,8 +307,8 @@ func (cli *CLI) RecipeList(ctx context.Context, envName string) (string, error)
}
// RecipeRegister runs a command to register a recipe with the given environment, template kind, template path and
-// link type, and returns the output string or an error.
-func (cli *CLI) RecipeRegister(ctx context.Context, envName, recipeName, templateKind, templatePath, linkType string) (string, error) {
+// resource type, and returns the output string or an error.
+func (cli *CLI) RecipeRegister(ctx context.Context, envName, recipeName, templateKind, templatePath, resourceType string) (string, error) {
args := []string{
"recipe",
"register",
@@ -316,32 +316,32 @@ func (cli *CLI) RecipeRegister(ctx context.Context, envName, recipeName, templat
"--environment", envName,
"--template-kind", templateKind,
"--template-path", templatePath,
- "--link-type", linkType,
+ "--resource-type", resourceType,
}
return cli.RunCommand(ctx, args)
}
-// RecipeUnregister runs a command to unregister a recipe from an environment, given the recipe name and link type.
+// RecipeUnregister runs a command to unregister a recipe from an environment, given the recipe name and resource type.
// It returns a string and an error if the command fails.
-func (cli *CLI) RecipeUnregister(ctx context.Context, envName, recipeName, linkType string) (string, error) {
+func (cli *CLI) RecipeUnregister(ctx context.Context, envName, recipeName, resourceType string) (string, error) {
args := []string{
"recipe",
"unregister",
recipeName,
- "--link-type", linkType,
+ "--resource-type", resourceType,
"--environment", envName,
}
return cli.RunCommand(ctx, args)
}
-// RecipeShow runs a command to show a recipe with the given environment name, recipe name and link type, and returns the
+// RecipeShow runs a command to show a recipe with the given environment name, recipe name and resource type, and returns the
// output string or an error.
-func (cli *CLI) RecipeShow(ctx context.Context, envName, recipeName string, linkType string) (string, error) {
+func (cli *CLI) RecipeShow(ctx context.Context, envName, recipeName string, resourceType string) (string, error) {
args := []string{
"recipe",
"show",
recipeName,
- "--link-type", linkType,
+ "--resource-type", resourceType,
"--environment", envName,
}
return cli.RunCommand(ctx, args)
diff --git a/typespec/Applications.Core/environments.tsp b/typespec/Applications.Core/environments.tsp
index 0c7a2a7c8f..cb97def854 100644
--- a/typespec/Applications.Core/environments.tsp
+++ b/typespec/Applications.Core/environments.tsp
@@ -114,8 +114,8 @@ model TerraformRecipeProperties extends RecipeProperties {
@doc("Represents the request body of the getmetadata action.")
model RecipeGetMetadata {
- @doc("Type of the link this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'")
- linkType: string;
+ @doc("Type of the resource this recipe can be consumed by. For example: 'Applications.Datastores/mongoDatabases'")
+ resourceType: string;
@doc("The name of the recipe registered to the environment")
name: string;
diff --git a/typespec/Applications.Core/examples/2022-03-15-privatepreview/Environments_GetRecipeMetadata.json b/typespec/Applications.Core/examples/2022-03-15-privatepreview/Environments_GetRecipeMetadata.json
index 1e5300771d..d17ab65f66 100644
--- a/typespec/Applications.Core/examples/2022-03-15-privatepreview/Environments_GetRecipeMetadata.json
+++ b/typespec/Applications.Core/examples/2022-03-15-privatepreview/Environments_GetRecipeMetadata.json
@@ -10,7 +10,7 @@
"responses": {
"200": {
"body": {
- "linkType": "Applications.Datastores/mongoDatabases",
+ "resourceType": "Applications.Datastores/mongoDatabases",
"templateKind": "bicep",
"templatePath": "br:sampleregistry.azureacr.io/radius/recipes/cosmosdb",
"parameters": {
diff --git a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_GetPlaneLocal.json b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_GetPlaneLocal.json
index de4215466c..ba3efe8c02 100644
--- a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_GetPlaneLocal.json
+++ b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_GetPlaneLocal.json
@@ -15,7 +15,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443"
},
"kind": "UCPNative"
diff --git a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_List.json b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_List.json
index 7a43a4d64e..2f22f7beb5 100644
--- a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_List.json
+++ b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_List.json
@@ -15,7 +15,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443",
"Microsoft.Resources": "http://bicep-de.radius-system:6443"
},
diff --git a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_ListPlanesByType.json b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_ListPlanesByType.json
index 3376f3b537..1544d77af2 100644
--- a/typespec/UCP/examples/2022-09-01-privatepreview/Planes_ListPlanesByType.json
+++ b/typespec/UCP/examples/2022-09-01-privatepreview/Planes_ListPlanesByType.json
@@ -16,7 +16,7 @@
"location": "global",
"properties": {
"resourceProviders": {
- "Applications.Link": "http://applications-rp.radius-system:5444",
+ "Applications.Datastores": "http://applications-rp.radius-system:5444",
"Applications.Core": "http://applications-rp.radius-system:5443"
},
"kind": "UCPNative"
From 88754fa6cdb21ed8a592211d17e55c6b0efed5af Mon Sep 17 00:00:00 2001
From: Aaron Crawfis
Date: Thu, 7 Sep 2023 16:23:31 -0700
Subject: [PATCH 08/13] Fix inconsistency in connection prefix naming for
container connections (#6235)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
This PR fixes a bug in the naming convention for connections to other
containers. Instead of "CONNECTIONS_" it should be "CONNECTION".
This PR also adds a unit test for this change.
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
## Auto-generated summary
### ๐ค Generated by Copilot at ba2d0ed
### Summary
๐ง๐๐งช
This pull request adds a test case for container-to-container routing
and refactors the environment variable keys for connection information
in `render.go` and `render_test.go`.
> _We forge the keys of connection_
> _With the power of `fmt.Sprintf`_
> _We test the routes of rendering_
> _With mock and assert in `render_test.go`_
### Walkthrough
* Refactor environment variable keys for connection information to use
consistent format
([link](https://github.com/radius-project/radius/pull/6235/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636L695-R702))
* Add variables for connection to containerB in test function
Test_Render_PortConnectedToRoute
([link](https://github.com/radius-project/radius/pull/6235/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94R500-R503))
* Pass connection entry for containerB to Render function in test
function Test_Render_Connections
([link](https://github.com/radius-project/radius/pull/6235/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94R515-R517))
* Assert expected environment variables for connection to containerB in
test function Test_Render_Connections
([link](https://github.com/radius-project/radius/pull/6235/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94R581-R592))
---
pkg/corerp/renderers/container/render.go | 10 +++++++---
pkg/corerp/renderers/container/render_test.go | 19 +++++++++++++++++++
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/pkg/corerp/renderers/container/render.go b/pkg/corerp/renderers/container/render.go
index cfac671c5f..a680a81af8 100644
--- a/pkg/corerp/renderers/container/render.go
+++ b/pkg/corerp/renderers/container/render.go
@@ -692,9 +692,13 @@ func getEnvVarsAndSecretData(resource *datamodel.ContainerResource, applicationN
return map[string]corev1.EnvVar{}, map[string][]byte{}, fmt.Errorf("failed to parse source URL: %w", err)
}
- env["CONNECTIONS_"+name+"_SCHEME"] = corev1.EnvVar{Name: "CONNECTIONS_" + name + "_SCHEME", Value: scheme}
- env["CONNECTIONS_"+name+"_HOSTNAME"] = corev1.EnvVar{Name: "CONNECTIONS_" + name + "_HOSTNAME", Value: hostname}
- env["CONNECTIONS_"+name+"_PORT"] = corev1.EnvVar{Name: "CONNECTIONS_" + name + "_PORT", Value: port}
+ schemeKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "SCHEME")
+ hostnameKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "HOSTNAME")
+ portKey := fmt.Sprintf("%s_%s_%s", "CONNECTION", strings.ToUpper(name), "PORT")
+
+ env[schemeKey] = corev1.EnvVar{Name: schemeKey, Value: scheme}
+ env[hostnameKey] = corev1.EnvVar{Name: hostnameKey, Value: hostname}
+ env[portKey] = corev1.EnvVar{Name: portKey, Value: port}
continue
}
diff --git a/pkg/corerp/renderers/container/render_test.go b/pkg/corerp/renderers/container/render_test.go
index 688151386e..de3673af7a 100644
--- a/pkg/corerp/renderers/container/render_test.go
+++ b/pkg/corerp/renderers/container/render_test.go
@@ -497,6 +497,10 @@ func Test_Render_PortConnectedToRoute(t *testing.T) {
}
func Test_Render_Connections(t *testing.T) {
+ containerConnectionHostname := "containerB"
+ containerConnectionScheme := "http"
+ containerConnectionPort := "80"
+
properties := datamodel.ContainerProperties{
BasicResourceProperties: rpv1.BasicResourceProperties{
Application: applicationResourceID,
@@ -508,6 +512,9 @@ func Test_Render_Connections(t *testing.T) {
Kind: datamodel.KindHTTP,
},
},
+ "containerB": {
+ Source: fmt.Sprintf("%s://%s:%s", containerConnectionScheme, containerConnectionHostname, containerConnectionPort),
+ },
},
Container: datamodel.Container{
Image: "someimage:latest",
@@ -571,6 +578,18 @@ func Test_Render_Connections(t *testing.T) {
},
},
},
+ {
+ Name: "CONNECTION_CONTAINERB_HOSTNAME",
+ Value: containerConnectionHostname,
+ },
+ {
+ Name: "CONNECTION_CONTAINERB_PORT",
+ Value: containerConnectionPort,
+ },
+ {
+ Name: "CONNECTION_CONTAINERB_SCHEME",
+ Value: containerConnectionScheme,
+ },
{Name: envVarName1, Value: envVarValue1},
{Name: envVarName2, Value: envVarValue2},
}
From e7d8971ecb61636df5d440fd45f288c5e88f12db Mon Sep 17 00:00:00 2001
From: Yetkin Timocin
Date: Thu, 7 Sep 2023 17:28:08 -0700
Subject: [PATCH 09/13] Adding postDeleteVerify to the Dapr functional tests
(#6195)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
1. Added `postDeleteVerify` steps to all the Dapr functional tests
2. Added NewDaprResource function to the functional test utils
3. Added a check of the Dapr components to the `K8Objects`
4. **!!! Added **Delete** to the **Processors** (We can talk about this
one and if we feel like we don't need this, I can find another way for
this)**
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #issue_number
## Auto-generated summary
### ๐ค Generated by Copilot at bffdd8e
### Summary
๐งน๐คโ
This pull request enhances the functional tests for the Dapr resources
by adding more validations and cleanup functions. It also fixes some
missing imports and simplifies some code in the `list.go` file.
Additionally, it adds a comment and a helper function for future work.
> _We're testing Dapr resources on the Kubernetes sea_
> _We're checking pub/sub, secret store and state store with
`K8sObject`s we create_
> _We're simplifying code and adding comments where we need_
> _We're heaving away on the count of three, we're the validation team_
### Walkthrough
* Simplify error handling in `WriteFormatted` method
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-260f1d886aae571f8487e5405785053daf07362bfef8b8bc6d9509900a3b3cc0L122-R122))
* Add `context` and `github.com/stretchr/testify/require` imports to
Dapr resource tests
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-9a6c029550d1c5b3bf1ec0c77525e20f26fe1ebb980b54ee20f1aaf253965922L20-R29),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-3c241abe05a99967207db1d0d573c292bd35add20b631d86573315e5fb71911fL20-R28),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-175c643bec9a553b195d07b69769dc0b86188779f6ddbafa79f0fd569de1f0b1L20-R29))
* Add validations for Dapr components created by manual and recipe tests
for Dapr pub/sub broker, secret store, and state store resources
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-9a6c029550d1c5b3bf1ec0c77525e20f26fe1ebb980b54ee20f1aaf253965922L59-R68),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-9a6c029550d1c5b3bf1ec0c77525e20f26fe1ebb980b54ee20f1aaf253965922L104-R124),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-3c241abe05a99967207db1d0d573c292bd35add20b631d86573315e5fb71911fR61-R64),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-3c241abe05a99967207db1d0d573c292bd35add20b631d86573315e5fb71911fL97-R115),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-175c643bec9a553b195d07b69769dc0b86188779f6ddbafa79f0fd569de1f0b1L61-R70),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-175c643bec9a553b195d07b69769dc0b86188779f6ddbafa79f0fd569de1f0b1L106-R126))
* Add `PostDeleteVerify` functions to check if Dapr resources are
deleted after tests
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-9a6c029550d1c5b3bf1ec0c77525e20f26fe1ebb980b54ee20f1aaf253965922L66-R82),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-9a6c029550d1c5b3bf1ec0c77525e20f26fe1ebb980b54ee20f1aaf253965922L110-R138),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-3c241abe05a99967207db1d0d573c292bd35add20b631d86573315e5fb71911fL63-R78),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-3c241abe05a99967207db1d0d573c292bd35add20b631d86573315e5fb71911fL103-R129),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-175c643bec9a553b195d07b69769dc0b86188779f6ddbafa79f0fd569de1f0b1L68-R84),
[link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-175c643bec9a553b195d07b69769dc0b86188779f6ddbafa79f0fd569de1f0b1L112-R140))
* Add helper function `NewDaprComponent` to create `K8sObject` for Dapr
components in `test/validation/k8s.go`
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-a52153bfeb452a3dd497767a98db0dc1a0621e012f6e6d8dd9d4b5ec758b4dc2R596-R608))
* Add comment to question the logic of `CheckRequiredFeatures` function
in `test/functional/shared/rptest.go`
([link](https://github.com/radius-project/radius/pull/6195/files?diff=unified&w=0#diff-f9ca7b9a91d5b71a3d9cbd29a179a8253f51f2755b89cceef405b46ecd03999bR199))
---
pkg/cli/cmd/group/list/list.go | 7 +-
pkg/corerp/backend/service.go | 9 +-
pkg/corerp/processors/extenders/processor.go | 5 +
.../processors/pubsubbrokers/processor.go | 36 ++++++
.../pubsubbrokers/processor_test.go | 4 +-
.../processors/secretstores/processor.go | 36 ++++++
.../processors/secretstores/processor_test.go | 4 +-
.../processors/statestores/processor.go | 36 ++++++
.../processors/statestores/processor_test.go | 4 +-
.../processors/mongodatabases/processor.go | 5 +
.../processors/rediscaches/processor.go | 5 +
.../processors/sqldatabases/processor.go | 5 +
.../processors/rabbitmqqueues/processor.go | 5 +
.../controller/createorupdateresource.go | 8 +-
.../controller/createorupdateresource_test.go | 23 ++--
.../backend/controller/deleteresource.go | 96 +++++++--------
.../backend/controller/deleteresource_test.go | 70 ++++-------
pkg/portableresources/backend/service.go | 116 +++++++++++++-----
pkg/portableresources/processors/types.go | 3 +
.../renderers/dapr/generic.go | 4 +-
pkg/portableresources/renderers/dapr/types.go | 22 ++++
test/functional/daprrp/common.go | 53 ++++++++
test/functional/daprrp/dapr_pubsub_test.go | 28 ++++-
.../daprrp/dapr_secretstore_test.go | 21 +++-
.../functional/daprrp/dapr_statestore_test.go | 28 ++++-
test/validation/k8s.go | 13 ++
26 files changed, 482 insertions(+), 164 deletions(-)
create mode 100644 pkg/portableresources/renderers/dapr/types.go
create mode 100644 test/functional/daprrp/common.go
diff --git a/pkg/cli/cmd/group/list/list.go b/pkg/cli/cmd/group/list/list.go
index 4ea61f8507..6bad81515a 100644
--- a/pkg/cli/cmd/group/list/list.go
+++ b/pkg/cli/cmd/group/list/list.go
@@ -119,10 +119,5 @@ func (r *Runner) Run(ctx context.Context) error {
return err
}
- err = r.Output.WriteFormatted(r.Format, resourceGroupDetails, objectformats.GetResourceGroupTableFormat())
-
- if err != nil {
- return err
- }
- return err
+ return r.Output.WriteFormatted(r.Format, resourceGroupDetails, objectformats.GetResourceGroupTableFormat())
}
diff --git a/pkg/corerp/backend/service.go b/pkg/corerp/backend/service.go
index 931846bcbc..e072b609b2 100644
--- a/pkg/corerp/backend/service.go
+++ b/pkg/corerp/backend/service.go
@@ -142,19 +142,22 @@ func (w *Service) Run(ctx context.Context) error {
processor := &extenders.Processor{}
return pr_backend_ctrl.NewCreateOrUpdateResource[*datamodel.Extender, datamodel.Extender](processor, engine, client, configLoader, options)
}
+ extenderDeleteController := func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &extenders.Processor{}
+ return pr_backend_ctrl.NewDeleteResource[*datamodel.Extender, datamodel.Extender](processor, engine, configLoader, options)
+ }
// Register controllers to run backend processing for extenders.
err = w.Controllers.Register(ctx, portableresources.ExtendersResourceType, v1.OperationPut, extenderCreateOrUpdateController, opts)
if err != nil {
return err
}
+
err = w.Controllers.Register(
ctx,
portableresources.ExtendersResourceType,
v1.OperationDelete,
- func(options ctrl.Options) (ctrl.Controller, error) {
- return pr_backend_ctrl.NewDeleteResource(options, engine)
- },
+ extenderDeleteController,
opts)
if err != nil {
return err
diff --git a/pkg/corerp/processors/extenders/processor.go b/pkg/corerp/processors/extenders/processor.go
index 9a2efc855e..59b08831b0 100644
--- a/pkg/corerp/processors/extenders/processor.go
+++ b/pkg/corerp/processors/extenders/processor.go
@@ -48,6 +48,11 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.Extender, o
return nil
}
+// Delete implements the processors.Processor interface for Extender resources.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.Extender, options processors.Options) error {
+ return nil
+}
+
func mergeOutputValues(properties map[string]any, recipeOutput *recipes.RecipeOutput, secret bool) map[string]any {
values := make(map[string]any)
for k, val := range properties {
diff --git a/pkg/daprrp/processors/pubsubbrokers/processor.go b/pkg/daprrp/processors/pubsubbrokers/processor.go
index 38fb525c41..a095416ca9 100644
--- a/pkg/daprrp/processors/pubsubbrokers/processor.go
+++ b/pkg/daprrp/processors/pubsubbrokers/processor.go
@@ -29,7 +29,9 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/pkg/ucp/resources"
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
runtime_client "sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -101,3 +103,37 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.DaprPubSubB
return nil
}
+
+// Delete implements the processors.Processor interface for DaprPubSubBroker resources. If the resource is being
+// provisioned manually, it deletes the Dapr component in Kubernetes.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.DaprPubSubBroker, options processors.Options) error {
+ if resource.Properties.ResourceProvisioning != portableresources.ResourceProvisioningManual {
+ // If the resource was provisioned by recipe then we expect the recipe engine to delete the Dapr Component
+ // in Kubernetes. At this point we're done so we can just return.
+ return nil
+ }
+
+ applicationID, err := resources.ParseResource(resource.Properties.Application)
+ if err != nil {
+ return err // This should already be validated by this point.
+ }
+
+ component := unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
+ "metadata": map[string]any{
+ "namespace": options.RuntimeConfiguration.Kubernetes.Namespace,
+ "name": kubernetes.NormalizeDaprResourceName(resource.Properties.ComponentName),
+ "labels": kubernetes.MakeDescriptiveDaprLabels(applicationID.Name(), resource.Name, portableresources.DaprPubSubBrokersResourceType),
+ },
+ },
+ }
+
+ err = p.Client.Delete(ctx, &component)
+ if err != nil {
+ return &processors.ResourceError{Inner: err}
+ }
+
+ return nil
+}
diff --git a/pkg/daprrp/processors/pubsubbrokers/processor_test.go b/pkg/daprrp/processors/pubsubbrokers/processor_test.go
index 62df14eed6..c4397b46c4 100644
--- a/pkg/daprrp/processors/pubsubbrokers/processor_test.go
+++ b/pkg/daprrp/processors/pubsubbrokers/processor_test.go
@@ -166,8 +166,8 @@ func Test_Process(t *testing.T) {
generated := &unstructured.Unstructured{
Object: map[string]any{
- "apiVersion": "dapr.io/v1alpha1",
- "kind": "Component",
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
"metadata": map[string]any{
"namespace": "test-namespace",
"name": "test-dapr-pubsub-broker",
diff --git a/pkg/daprrp/processors/secretstores/processor.go b/pkg/daprrp/processors/secretstores/processor.go
index b29fb7327b..7153006db2 100644
--- a/pkg/daprrp/processors/secretstores/processor.go
+++ b/pkg/daprrp/processors/secretstores/processor.go
@@ -29,7 +29,9 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/pkg/ucp/resources"
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
runtime_client "sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -98,3 +100,37 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.DaprSecretS
return nil
}
+
+// Delete implements the processors.Processor interface for DaprSecretStore resources. If the resource is being
+// provisioned manually, it deletes the Dapr component in Kubernetes.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.DaprSecretStore, options processors.Options) error {
+ if resource.Properties.ResourceProvisioning != portableresources.ResourceProvisioningManual {
+ // If the resource was provisioned by recipe then we expect the recipe engine to delete the Dapr Component
+ // in Kubernetes. At this point we're done so we can just return.
+ return nil
+ }
+
+ applicationID, err := resources.ParseResource(resource.Properties.Application)
+ if err != nil {
+ return err // This should already be validated by this point.
+ }
+
+ component := unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
+ "metadata": map[string]any{
+ "namespace": options.RuntimeConfiguration.Kubernetes.Namespace,
+ "name": kubernetes.NormalizeDaprResourceName(resource.Properties.ComponentName),
+ "labels": kubernetes.MakeDescriptiveDaprLabels(applicationID.Name(), resource.Name, portableresources.DaprSecretStoresResourceType),
+ },
+ },
+ }
+
+ err = p.Client.Delete(ctx, &component)
+ if err != nil {
+ return &processors.ResourceError{Inner: err}
+ }
+
+ return nil
+}
diff --git a/pkg/daprrp/processors/secretstores/processor_test.go b/pkg/daprrp/processors/secretstores/processor_test.go
index 2dfbdd785d..e5eedf30c5 100644
--- a/pkg/daprrp/processors/secretstores/processor_test.go
+++ b/pkg/daprrp/processors/secretstores/processor_test.go
@@ -150,8 +150,8 @@ func Test_Process(t *testing.T) {
expectedSecrets := map[string]rpv1.SecretValueReference{}
generated := &unstructured.Unstructured{
Object: map[string]any{
- "apiVersion": "dapr.io/v1alpha1",
- "kind": "Component",
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
"metadata": map[string]any{
"namespace": "test-namespace",
"name": "test-component",
diff --git a/pkg/daprrp/processors/statestores/processor.go b/pkg/daprrp/processors/statestores/processor.go
index 7b242d6934..5275e92864 100644
--- a/pkg/daprrp/processors/statestores/processor.go
+++ b/pkg/daprrp/processors/statestores/processor.go
@@ -29,7 +29,9 @@ import (
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/pkg/ucp/resources"
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
runtime_client "sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -101,3 +103,37 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.DaprStateSt
return nil
}
+
+// Delete implements the processors.Processor interface for DaprStateStore resources. If the resource is being
+// provisioned manually, it deletes the Dapr component in Kubernetes.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.DaprStateStore, options processors.Options) error {
+ if resource.Properties.ResourceProvisioning != portableresources.ResourceProvisioningManual {
+ // If the resource was provisioned by recipe then we expect the recipe engine to delete the Dapr Component
+ // in Kubernetes. At this point we're done so we can just return.
+ return nil
+ }
+
+ applicationID, err := resources.ParseResource(resource.Properties.Application)
+ if err != nil {
+ return err // This should already be validated by this point.
+ }
+
+ component := unstructured.Unstructured{
+ Object: map[string]any{
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
+ "metadata": map[string]any{
+ "namespace": options.RuntimeConfiguration.Kubernetes.Namespace,
+ "name": kubernetes.NormalizeDaprResourceName(resource.Properties.ComponentName),
+ "labels": kubernetes.MakeDescriptiveDaprLabels(applicationID.Name(), resource.Name, portableresources.DaprStateStoresResourceType),
+ },
+ },
+ }
+
+ err = p.Client.Delete(ctx, &component)
+ if err != nil {
+ return &processors.ResourceError{Inner: err}
+ }
+
+ return nil
+}
diff --git a/pkg/daprrp/processors/statestores/processor_test.go b/pkg/daprrp/processors/statestores/processor_test.go
index 3c40e6b2ff..ddb228d04f 100644
--- a/pkg/daprrp/processors/statestores/processor_test.go
+++ b/pkg/daprrp/processors/statestores/processor_test.go
@@ -158,8 +158,8 @@ func Test_Process(t *testing.T) {
generated := &unstructured.Unstructured{
Object: map[string]any{
- "apiVersion": "dapr.io/v1alpha1",
- "kind": "Component",
+ "apiVersion": dapr.DaprAPIVersion,
+ "kind": dapr.DaprKind,
"metadata": map[string]any{
"namespace": "test-namespace",
"name": "test-component",
diff --git a/pkg/datastoresrp/processors/mongodatabases/processor.go b/pkg/datastoresrp/processors/mongodatabases/processor.go
index 2acce23c5e..3b54fbd52b 100644
--- a/pkg/datastoresrp/processors/mongodatabases/processor.go
+++ b/pkg/datastoresrp/processors/mongodatabases/processor.go
@@ -52,6 +52,11 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.MongoDataba
return nil
}
+// Delete implements the processors.Processor interface for MongoDatabase resources.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.MongoDatabase, options processors.Options) error {
+ return nil
+}
+
func (p *Processor) computeConnectionString(resource *datamodel.MongoDatabase) string {
connectionString := "mongodb://"
diff --git a/pkg/datastoresrp/processors/rediscaches/processor.go b/pkg/datastoresrp/processors/rediscaches/processor.go
index 9c04d791a3..94f43bfdfb 100644
--- a/pkg/datastoresrp/processors/rediscaches/processor.go
+++ b/pkg/datastoresrp/processors/rediscaches/processor.go
@@ -65,6 +65,11 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.RedisCache,
return nil
}
+// Delete implements the processors.Processor interface for RedisCache resources.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.RedisCache, options processors.Options) error {
+ return nil
+}
+
func (p *Processor) computeSSL(resource *datamodel.RedisCache) bool {
return resource.Properties.Port == RedisSSLPort
}
diff --git a/pkg/datastoresrp/processors/sqldatabases/processor.go b/pkg/datastoresrp/processors/sqldatabases/processor.go
index 3b974d2b85..cd5e8768e4 100644
--- a/pkg/datastoresrp/processors/sqldatabases/processor.go
+++ b/pkg/datastoresrp/processors/sqldatabases/processor.go
@@ -41,6 +41,11 @@ func (p *Processor) Process(ctx context.Context, resource *datamodel.SqlDatabase
return nil
}
+// Delete implements the processors.Processor interface for SQLDatabase resources.
+func (p *Processor) Delete(ctx context.Context, resource *datamodel.SqlDatabase, options processors.Options) error {
+ return nil
+}
+
func (p *Processor) computeConnectionString(resource *datamodel.SqlDatabase) string {
var username, password string
if resource.Properties.Username != "" {
diff --git a/pkg/messagingrp/processors/rabbitmqqueues/processor.go b/pkg/messagingrp/processors/rabbitmqqueues/processor.go
index 683ea9e9aa..093b50f8df 100644
--- a/pkg/messagingrp/processors/rabbitmqqueues/processor.go
+++ b/pkg/messagingrp/processors/rabbitmqqueues/processor.go
@@ -61,6 +61,11 @@ func (p *Processor) Process(ctx context.Context, resource *msg_dm.RabbitMQQueue,
return nil
}
+// Delete implements the processors.Processor interface for RabbitMQQueue resources.
+func (p *Processor) Delete(ctx context.Context, resource *msg_dm.RabbitMQQueue, options processors.Options) error {
+ return nil
+}
+
func (p *Processor) computeURI(resource *msg_dm.RabbitMQQueue) string {
rabbitMQProtocol := "amqp"
if resource.Properties.TLS {
diff --git a/pkg/portableresources/backend/controller/createorupdateresource.go b/pkg/portableresources/backend/controller/createorupdateresource.go
index 24a77d94bf..d41de121d2 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource.go
@@ -48,7 +48,13 @@ func NewCreateOrUpdateResource[P interface {
*T
rpv1.RadiusResourceModel
}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, client processors.ResourceClient, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) {
- return &CreateOrUpdateResource[P, T]{ctrl.NewBaseAsyncController(opts), processor, eng, client, configurationLoader}, nil
+ return &CreateOrUpdateResource[P, T]{
+ ctrl.NewBaseAsyncController(opts),
+ processor,
+ eng,
+ client,
+ configurationLoader,
+ }, nil
}
// Run retrieves an existing resource, executes a recipe if needed, loads runtime configuration,
diff --git a/pkg/portableresources/backend/controller/createorupdateresource_test.go b/pkg/portableresources/backend/controller/createorupdateresource_test.go
index 188bca0f75..6a15df3873 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource_test.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource_test.go
@@ -98,6 +98,11 @@ func (p *SuccessProcessor) Process(ctx context.Context, data *TestResource, opti
return nil
}
+// Delete returns no error.
+func (p *SuccessProcessor) Delete(ctx context.Context, data *TestResource, options processors.Options) error {
+ return nil
+}
+
var successProcessorReference = processors.ResourceProcessor[*TestResource, TestResource](&SuccessProcessor{})
type ErrorProcessor struct {
@@ -105,12 +110,16 @@ type ErrorProcessor struct {
// Process always returns a processorErr.
func (p *ErrorProcessor) Process(ctx context.Context, data *TestResource, options processors.Options) error {
- return processorErr
+ return errProcessor
+}
+
+func (p *ErrorProcessor) Delete(ctx context.Context, data *TestResource, options processors.Options) error {
+ return nil
}
var errorProcessorReference = processors.ResourceProcessor[*TestResource, TestResource](&ErrorProcessor{})
-var processorErr = errors.New("processor error")
-var configurationErr = errors.New("configuration error")
+var errProcessor = errors.New("processor error")
+var errConfiguration = errors.New("configuration error")
var oldOutputResourceResourceID = "/subscriptions/test-sub/resourceGroups/test-rg/providers/System.Test/testResources/test1"
@@ -204,11 +213,11 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
nil,
false,
nil,
- configurationErr,
+ errConfiguration,
nil,
nil,
nil,
- configurationErr,
+ errConfiguration,
},
{
"processor-err",
@@ -219,10 +228,10 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
false,
nil,
nil,
- processorErr,
+ errProcessor,
nil,
nil,
- processorErr,
+ errProcessor,
},
{
"save-err",
diff --git a/pkg/portableresources/backend/controller/deleteresource.go b/pkg/portableresources/backend/controller/deleteresource.go
index c8cf805942..d33ed3ea4f 100644
--- a/pkg/portableresources/backend/controller/deleteresource.go
+++ b/pkg/portableresources/backend/controller/deleteresource.go
@@ -18,39 +18,45 @@ package controller
import (
"context"
- "fmt"
- "strings"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
- ext_dm "github.com/radius-project/radius/pkg/corerp/datamodel"
- dapr_dm "github.com/radius-project/radius/pkg/daprrp/datamodel"
- ds_dm "github.com/radius-project/radius/pkg/datastoresrp/datamodel"
- msg_dm "github.com/radius-project/radius/pkg/messagingrp/datamodel"
- "github.com/radius-project/radius/pkg/portableresources"
"github.com/radius-project/radius/pkg/portableresources/datamodel"
+ "github.com/radius-project/radius/pkg/portableresources/processors"
"github.com/radius-project/radius/pkg/recipes"
+ "github.com/radius-project/radius/pkg/recipes/configloader"
"github.com/radius-project/radius/pkg/recipes/engine"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/ucp/resources"
)
-var _ ctrl.Controller = (*DeleteResource)(nil)
-
// DeleteResource is the async operation controller to delete a portable resource.
-type DeleteResource struct {
+type DeleteResource[P interface {
+ *T
+ rpv1.RadiusResourceModel
+}, T any] struct {
ctrl.BaseController
- engine engine.Engine
+ processor processors.ResourceProcessor[P, T]
+ engine engine.Engine
+ configurationLoader configloader.ConfigurationLoader
}
// NewDeleteResource creates a new DeleteResource controller which is used to delete resources asynchronously.
-func NewDeleteResource(opts ctrl.Options, engine engine.Engine) (ctrl.Controller, error) {
- return &DeleteResource{ctrl.NewBaseAsyncController(opts), engine}, nil
+func NewDeleteResource[P interface {
+ *T
+ rpv1.RadiusResourceModel
+}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) {
+ return &DeleteResource[P, T]{
+ ctrl.NewBaseAsyncController(opts),
+ processor,
+ eng,
+ configurationLoader,
+ }, nil
}
// Run retrieves a resource from storage, parses the resource ID, gets the data model, deletes the output
// resources, and deletes the resource from storage. It returns an error if any of these steps fail.
-func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.Result, error) {
+func (c *DeleteResource[P, T]) Run(ctx context.Context, request *ctrl.Request) (ctrl.Result, error) {
obj, err := c.StorageClient().Get(ctx, request.ResourceID)
if err != nil {
return ctrl.NewFailedResult(v1.ErrorDetails{Message: err.Error()}), err
@@ -62,31 +68,22 @@ func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.R
return ctrl.Result{}, err
}
- dataModel, err := getDataModel(id)
- if err != nil {
- return ctrl.Result{}, err
- }
-
- if err = obj.As(dataModel); err != nil {
+ data := P(new(T))
+ if err = obj.As(data); err != nil {
return ctrl.Result{}, err
}
- resourceDataModel, ok := dataModel.(rpv1.RadiusResourceModel)
- if !ok {
- return ctrl.NewFailedResult(v1.ErrorDetails{Message: "deployment data model conversion error"}), nil
- }
-
- recipeDataModel, supportsRecipes := dataModel.(datamodel.RecipeDataModel)
+ recipeDataModel, supportsRecipes := any(data).(datamodel.RecipeDataModel)
if supportsRecipes && recipeDataModel.Recipe() != nil {
recipeData := recipes.ResourceMetadata{
Name: recipeDataModel.Recipe().Name,
- EnvironmentID: resourceDataModel.ResourceMetadata().Environment,
- ApplicationID: resourceDataModel.ResourceMetadata().Application,
+ EnvironmentID: data.ResourceMetadata().Environment,
+ ApplicationID: data.ResourceMetadata().Application,
Parameters: recipeDataModel.Recipe().Parameters,
ResourceID: id.String(),
}
- err = c.engine.Delete(ctx, recipeData, resourceDataModel.OutputResources())
+ err = c.engine.Delete(ctx, recipeData, data.OutputResources())
if err != nil {
if recipeError, ok := err.(*recipes.RecipeError); ok {
return ctrl.NewFailedResult(recipeError.ErrorDetails), nil
@@ -95,6 +92,19 @@ func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.R
}
}
+ // Load details about the runtime for the processor to access.
+ runtimeConfiguration, err := c.loadRuntimeConfiguration(ctx, data.ResourceMetadata().Environment, data.ResourceMetadata().Application, data.GetBaseResource().ID)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ err = c.processor.Delete(ctx, data, processors.Options{
+ RuntimeConfiguration: *runtimeConfiguration,
+ })
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
err = c.StorageClient().Delete(ctx, request.ResourceID)
if err != nil {
return ctrl.Result{}, err
@@ -103,26 +113,12 @@ func (c *DeleteResource) Run(ctx context.Context, request *ctrl.Request) (ctrl.R
return ctrl.Result{}, err
}
-func getDataModel(id resources.ID) (v1.ResourceDataModel, error) {
- resourceType := strings.ToLower(id.Type())
- switch resourceType {
- case strings.ToLower(portableresources.MongoDatabasesResourceType):
- return &ds_dm.MongoDatabase{}, nil
- case strings.ToLower(portableresources.RedisCachesResourceType):
- return &ds_dm.RedisCache{}, nil
- case strings.ToLower(portableresources.SqlDatabasesResourceType):
- return &ds_dm.SqlDatabase{}, nil
- case strings.ToLower(portableresources.DaprStateStoresResourceType):
- return &dapr_dm.DaprStateStore{}, nil
- case strings.ToLower(portableresources.RabbitMQQueuesResourceType):
- return &msg_dm.RabbitMQQueue{}, nil
- case strings.ToLower(portableresources.DaprSecretStoresResourceType):
- return &dapr_dm.DaprSecretStore{}, nil
- case strings.ToLower(portableresources.DaprPubSubBrokersResourceType):
- return &dapr_dm.DaprPubSubBroker{}, nil
- case strings.ToLower(portableresources.ExtendersResourceType):
- return &ext_dm.Extender{}, nil
- default:
- return nil, fmt.Errorf("async delete operation unsupported on resource type: %q. Resource ID: %q", resourceType, id.String())
+func (c *DeleteResource[P, T]) loadRuntimeConfiguration(ctx context.Context, environmentID string, applicationID string, resourceID string) (*recipes.RuntimeConfiguration, error) {
+ metadata := recipes.ResourceMetadata{EnvironmentID: environmentID, ApplicationID: applicationID, ResourceID: resourceID}
+ config, err := c.configurationLoader.LoadConfiguration(ctx, metadata)
+ if err != nil {
+ return nil, err
}
+
+ return &config.Runtime, nil
}
diff --git a/pkg/portableresources/backend/controller/deleteresource_test.go b/pkg/portableresources/backend/controller/deleteresource_test.go
index a005bc3214..e101f56525 100644
--- a/pkg/portableresources/backend/controller/deleteresource_test.go
+++ b/pkg/portableresources/backend/controller/deleteresource_test.go
@@ -27,6 +27,7 @@ import (
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
"github.com/radius-project/radius/pkg/recipes"
+ "github.com/radius-project/radius/pkg/recipes/configloader"
"github.com/radius-project/radius/pkg/recipes/engine"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/to"
@@ -43,11 +44,11 @@ var outputResource = rpv1.OutputResource{
func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
resourceID := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Datastores/mongoDatabases/mongo0"
- setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *ctrl.Request, *engine.MockEngine) {
+ setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *ctrl.Request, *engine.MockEngine, *configloader.MockConfigurationLoader) {
mctrl := gomock.NewController(t)
-
msc := store.NewMockStorageClient(mctrl)
eng := engine.NewMockEngine(mctrl)
+ cfg := configloader.NewMockConfigurationLoader(mctrl)
req := &ctrl.Request{
OperationID: uuid.New(),
@@ -59,7 +60,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
return func(tb testing.TB) {
mctrl.Finish()
- }, msc, req, eng
+ }, msc, req, eng, cfg
}
t.Parallel()
@@ -78,7 +79,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
for _, tt := range deleteCases {
t.Run(tt.desc, func(t *testing.T) {
- teardownTest, msc, req, eng := setupTest(t)
+ teardownTest, msc, req, eng, configLoader := setupTest(t)
defer teardownTest(t)
status := rpv1.ResourceStatus{
@@ -125,6 +126,21 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
Return(tt.engDelErr).
Times(1)
if tt.engDelErr == nil {
+ configLoader.EXPECT().
+ LoadConfiguration(gomock.Any(), gomock.Any()).
+ Return(
+ &recipes.Configuration{
+ Runtime: recipes.RuntimeConfiguration{
+ Kubernetes: &recipes.KubernetesRuntime{
+ Namespace: "test-namespace",
+ EnvironmentNamespace: "test-env-namespace",
+ },
+ },
+ },
+ nil,
+ ).
+ Times(1)
+
msc.EXPECT().
Delete(gomock.Any(), gomock.Any()).
Return(tt.scDelErr).
@@ -135,7 +151,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
StorageClient: msc,
}
- ctrl, err := NewDeleteResource(opts, eng)
+ ctrl, err := NewDeleteResource(successProcessorReference, eng, configLoader, opts)
require.NoError(t, err)
_, err = ctrl.Run(context.Background(), req)
@@ -148,47 +164,3 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
})
}
}
-
-func TestDeleteResourceRunInvalidResourceType_20220315PrivatePreview(t *testing.T) {
-
- setupTest := func(tb testing.TB) (func(tb testing.TB), *store.MockStorageClient, *ctrl.Request, *gomock.Controller) {
- mctrl := gomock.NewController(t)
-
- msc := store.NewMockStorageClient(mctrl)
-
- req := &ctrl.Request{
- OperationID: uuid.New(),
- OperationType: "APPLICATIONS.DAPR/INVALID|DELETE",
- ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Dapr/invalidType/invalid",
- CorrelationID: uuid.NewString(),
- OperationTimeout: &ctrl.DefaultAsyncOperationTimeout,
- }
-
- return func(tb testing.TB) {
- mctrl.Finish()
- }, msc, req, mctrl
- }
-
- t.Parallel()
-
- t.Run("deleting-invalid-resource", func(t *testing.T) {
- teardownTest, msc, req, mctrl := setupTest(t)
- defer teardownTest(t)
-
- msc.EXPECT().
- Get(gomock.Any(), gomock.Any()).
- Return(&store.Object{}, nil).
- Times(1)
- opts := ctrl.Options{
- StorageClient: msc,
- }
-
- eng := engine.NewMockEngine(mctrl)
- ctrl, err := NewDeleteResource(opts, eng)
- require.NoError(t, err)
-
- _, err = ctrl.Run(context.Background(), req)
- require.Error(t, err)
- require.Equal(t, "async delete operation unsupported on resource type: \"applications.dapr/invalidtype\". Resource ID: \"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Dapr/invalidType/invalid\"", err.Error())
- })
-}
diff --git a/pkg/portableresources/backend/service.go b/pkg/portableresources/backend/service.go
index 72e40542a4..440765cc94 100644
--- a/pkg/portableresources/backend/service.go
+++ b/pkg/portableresources/backend/service.go
@@ -103,37 +103,87 @@ func (s *Service) Run(ctx context.Context) error {
// resourceTypes is the array that holds resource types that needs async processing.
// We use this array to register backend controllers for each resource.
resourceTypes := []struct {
- TypeName string
- CreatePutController func(options ctrl.Options) (ctrl.Controller, error)
+ TypeName string
+ CreatePutController func(options ctrl.Options) (ctrl.Controller, error)
+ CreateDeleteController func(options ctrl.Options) (ctrl.Controller, error)
}{
- {portableresources.RabbitMQQueuesResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &rabbitmqqueues.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, client, configLoader, options)
- }},
- {portableresources.DaprStateStoresResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &statestores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, client, configLoader, options)
- }},
- {portableresources.DaprSecretStoresResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &secretstores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, client, configLoader, options)
- }},
- {portableresources.DaprPubSubBrokersResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &pubsubbrokers.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, client, configLoader, options)
- }},
- {portableresources.MongoDatabasesResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &mongo_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, client, configLoader, options)
- }},
- {portableresources.RedisCachesResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &redis_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, client, configLoader, options)
- }},
- {portableresources.SqlDatabasesResourceType, func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &sql_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, client, configLoader, options)
- }},
+ {
+ portableresources.RabbitMQQueuesResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &rabbitmqqueues.Processor{}
+ return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &rabbitmqqueues.Processor{}
+ return backend_ctrl.NewDeleteResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.DaprStateStoresResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &statestores.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &statestores.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.DaprSecretStoresResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &secretstores.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &secretstores.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.DaprPubSubBrokersResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &pubsubbrokers.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &pubsubbrokers.Processor{Client: s.KubeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.MongoDatabasesResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &mongo_prc.Processor{}
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &mongo_prc.Processor{}
+ return backend_ctrl.NewDeleteResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.RedisCachesResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &redis_prc.Processor{}
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &redis_prc.Processor{}
+ return backend_ctrl.NewDeleteResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, configLoader, options)
+ },
+ },
+ {
+ portableresources.SqlDatabasesResourceType,
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &sql_prc.Processor{}
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, client, configLoader, options)
+ },
+ func(options ctrl.Options) (ctrl.Controller, error) {
+ processor := &sql_prc.Processor{}
+ return backend_ctrl.NewDeleteResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, configLoader, options)
+ },
+ },
}
opts := ctrl.Options{
@@ -143,17 +193,17 @@ func (s *Service) Run(ctx context.Context) error {
for _, rt := range resourceTypes {
// Register controllers
- err = s.Controllers.Register(ctx, rt.TypeName, v1.OperationDelete, func(options ctrl.Options) (ctrl.Controller, error) {
- return backend_ctrl.NewDeleteResource(options, engine)
- }, opts)
+ err = s.Controllers.Register(ctx, rt.TypeName, v1.OperationDelete, rt.CreateDeleteController, opts)
if err != nil {
return err
}
+
err = s.Controllers.Register(ctx, rt.TypeName, v1.OperationPut, rt.CreatePutController, opts)
if err != nil {
return err
}
}
+
workerOpts := worker.Options{}
if s.Options.Config.WorkerServer != nil {
if s.Options.Config.WorkerServer.MaxOperationConcurrency != nil {
diff --git a/pkg/portableresources/processors/types.go b/pkg/portableresources/processors/types.go
index 56224318d2..78f9c1cb44 100644
--- a/pkg/portableresources/processors/types.go
+++ b/pkg/portableresources/processors/types.go
@@ -34,6 +34,9 @@ type ResourceProcessor[P interface {
// Process is called to process the results of recipe execution or any other changes to the resource
// data model. Process should modify the datamodel in place to perform updates.
Process(ctx context.Context, resource P, options Options) error
+
+ // Delete is called to delete all the resources created by the resource processor.
+ Delete(ctx context.Context, resource P, options Options) error
}
// Options defines the options passed to the resource processor.
diff --git a/pkg/portableresources/renderers/dapr/generic.go b/pkg/portableresources/renderers/dapr/generic.go
index 54106785e5..4fe26dbc9c 100644
--- a/pkg/portableresources/renderers/dapr/generic.go
+++ b/pkg/portableresources/renderers/dapr/generic.go
@@ -66,8 +66,8 @@ func ConstructDaprGeneric(daprGeneric DaprGeneric, namespace string, componentNa
// Translate into Dapr State Store schema
item := unstructured.Unstructured{
Object: map[string]any{
- "apiVersion": "dapr.io/v1alpha1",
- "kind": "Component",
+ "apiVersion": DaprAPIVersion,
+ "kind": DaprKind,
"metadata": map[string]any{
"namespace": namespace,
"name": kubernetes.NormalizeDaprResourceName(componentName),
diff --git a/pkg/portableresources/renderers/dapr/types.go b/pkg/portableresources/renderers/dapr/types.go
new file mode 100644
index 0000000000..c5819752f7
--- /dev/null
+++ b/pkg/portableresources/renderers/dapr/types.go
@@ -0,0 +1,22 @@
+/*
+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 dapr
+
+const (
+ DaprAPIVersion = "dapr.io/v1alpha1"
+ DaprKind = "Component"
+)
diff --git a/test/functional/daprrp/common.go b/test/functional/daprrp/common.go
new file mode 100644
index 0000000000..866a921d04
--- /dev/null
+++ b/test/functional/daprrp/common.go
@@ -0,0 +1,53 @@
+/*
+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 resource_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/radius-project/radius/pkg/cli/clients"
+ "github.com/radius-project/radius/pkg/cli/clients_new/generated"
+ "github.com/radius-project/radius/test/functional/shared"
+
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
+)
+
+func verifyDaprComponentsDeleted(ctx context.Context, t *testing.T, test shared.RPTest, resourceType, resourceName, namespace string) {
+ resource, err := test.Options.ManagementClient.ShowResource(ctx, resourceType, resourceName)
+ require.Error(t, err)
+ require.True(t, clients.Is404Error(err))
+ require.Equal(t, generated.GenericResource{}, resource)
+
+ dynamicClient, err := dynamic.NewForConfig(test.Options.K8sConfig)
+ require.NoError(t, err)
+
+ gvr := schema.GroupVersionResource{
+ Group: "dapr.io",
+ Version: "v1alpha1",
+ Resource: "components",
+ }
+
+ resourceList, err := dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{
+ LabelSelector: "radius.dev/resource=" + resourceName,
+ })
+ require.NoError(t, err)
+ require.Equal(t, 0, len(resourceList.Items))
+}
diff --git a/test/functional/daprrp/dapr_pubsub_test.go b/test/functional/daprrp/dapr_pubsub_test.go
index 7b286c74ae..e06d2664bb 100644
--- a/test/functional/daprrp/dapr_pubsub_test.go
+++ b/test/functional/daprrp/dapr_pubsub_test.go
@@ -17,6 +17,7 @@ limitations under the License.
package resource_test
import (
+ "context"
"fmt"
"testing"
@@ -56,14 +57,25 @@ func Test_DaprPubSubBroker_Manual(t *testing.T) {
Namespaces: map[string][]validation.K8sObject{
appNamespace: {
validation.NewK8sPodForResource(name, "dpsb-manual-app-ctnr"),
- validation.NewK8sPodForResource(name, "dpsb-manual-redis").ValidateLabels(false),
- validation.NewK8sServiceForResource(name, "dpsb-manual-redis").ValidateLabels(false),
+ validation.NewK8sPodForResource(name, "dpsb-manual-redis").
+ ValidateLabels(false),
+ validation.NewK8sServiceForResource(name, "dpsb-manual-redis").
+ ValidateLabels(false),
+
+ validation.NewDaprComponent(name, "dpsb-manual").
+ ValidateLabels(false),
},
},
},
},
})
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/pubSubBrokers", "dpsb-manual", appNamespace)
+ }
+
test.Test(t)
}
@@ -101,12 +113,22 @@ func Test_DaprPubSubBroker_Recipe(t *testing.T) {
K8sObjects: &validation.K8sObjectSet{
Namespaces: map[string][]validation.K8sObject{
appNamespace: {
- validation.NewK8sPodForResource(name, "dpsb-recipe-ctnr").ValidateLabels(false),
+ validation.NewK8sPodForResource(name, "dpsb-recipe-ctnr").
+ ValidateLabels(false),
+
+ validation.NewDaprComponent(name, "dpsb-recipe").
+ ValidateLabels(false),
},
},
},
},
})
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/pubSubBrokers", "dpsb-recipe", appNamespace)
+ }
+
test.Test(t)
}
diff --git a/test/functional/daprrp/dapr_secretstore_test.go b/test/functional/daprrp/dapr_secretstore_test.go
index c7665acf31..490b182374 100644
--- a/test/functional/daprrp/dapr_secretstore_test.go
+++ b/test/functional/daprrp/dapr_secretstore_test.go
@@ -17,6 +17,7 @@ limitations under the License.
package resource_test
import (
+ "context"
"testing"
"github.com/radius-project/radius/test/functional"
@@ -55,13 +56,22 @@ func Test_DaprSecretStore_Manual(t *testing.T) {
Namespaces: map[string][]validation.K8sObject{
appNamespace: {
validation.NewK8sPodForResource(name, "gnrc-scs-ctnr"),
+
+ // Not sure why we skip validating the labels
+ validation.NewDaprComponent(name, "gnrc-scs-manual").
+ ValidateLabels(false),
},
},
},
},
}, shared.K8sSecretResource(appNamespace, "mysecret", "", "fakekey", []byte("fakevalue")))
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/secretStores", "gnrc-scs-manual", appNamespace)
+ }
+
test.Test(t)
}
@@ -94,13 +104,22 @@ func Test_DaprSecretStore_Recipe(t *testing.T) {
K8sObjects: &validation.K8sObjectSet{
Namespaces: map[string][]validation.K8sObject{
appNamespace: {
- validation.NewK8sPodForResource(name, "gnrc-scs-ctnr-recipe").ValidateLabels(false),
+ validation.NewK8sPodForResource(name, "gnrc-scs-ctnr-recipe").
+ ValidateLabels(false),
+
+ validation.NewDaprComponent(name, "gnrc-scs-recipe").
+ ValidateLabels(false),
},
},
},
},
}, shared.K8sSecretResource(appNamespace, "mysecret", "", "fakekey", []byte("fakevalue")))
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/secretStores", "gnrc-scs-recipe", appNamespace)
+ }
+
test.Test(t)
}
diff --git a/test/functional/daprrp/dapr_statestore_test.go b/test/functional/daprrp/dapr_statestore_test.go
index 4dc9a5282d..73efcb73d3 100644
--- a/test/functional/daprrp/dapr_statestore_test.go
+++ b/test/functional/daprrp/dapr_statestore_test.go
@@ -17,6 +17,7 @@ limitations under the License.
package resource_test
import (
+ "context"
"fmt"
"testing"
@@ -58,14 +59,25 @@ func Test_DaprStateStore_Manual(t *testing.T) {
validation.NewK8sPodForResource(name, "dapr-sts-manual-ctnr"),
// Deployed as supporting resources using Kubernetes Bicep extensibility.
- validation.NewK8sPodForResource(name, "dapr-sts-manual-redis").ValidateLabels(false),
- validation.NewK8sServiceForResource(name, "dapr-sts-manual-redis").ValidateLabels(false),
+ validation.NewK8sPodForResource(name, "dapr-sts-manual-redis").
+ ValidateLabels(false),
+ validation.NewK8sServiceForResource(name, "dapr-sts-manual-redis").
+ ValidateLabels(false),
+
+ validation.NewDaprComponent(name, "dapr-sts-manual").
+ ValidateLabels(false),
},
},
},
},
})
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/stateStores", "dapr-sts-manual", appNamespace)
+ }
+
test.Test(t)
}
@@ -103,12 +115,22 @@ func Test_DaprStateStore_Recipe(t *testing.T) {
K8sObjects: &validation.K8sObjectSet{
Namespaces: map[string][]validation.K8sObject{
appNamespace: {
- validation.NewK8sPodForResource(name, "dapr-sts-recipe-ctnr").ValidateLabels(false),
+ validation.NewK8sPodForResource(name, "dapr-sts-recipe-ctnr").
+ ValidateLabels(false),
+
+ validation.NewDaprComponent(name, "dapr-sts-recipe").
+ ValidateLabels(false),
},
},
},
},
})
+
test.RequiredFeatures = []shared.RequiredFeature{shared.FeatureDapr}
+
+ test.PostDeleteVerify = func(ctx context.Context, t *testing.T, test shared.RPTest) {
+ verifyDaprComponentsDeleted(ctx, t, test, "Applications.Dapr/stateStores", "dapr-sts-recipe", appNamespace)
+ }
+
test.Test(t)
}
diff --git a/test/validation/k8s.go b/test/validation/k8s.go
index 0772efd7e1..a75c33d177 100644
--- a/test/validation/k8s.go
+++ b/test/validation/k8s.go
@@ -593,3 +593,16 @@ func labelsEqual(expectedLabels map[string]string, actualLabels map[string]strin
}
return true
}
+
+// NewDaprComponent creates a K8sObject for a Dapr component with the Labels set to the application and name.
+func NewDaprComponent(application string, name string) K8sObject {
+ return K8sObject{
+ GroupVersionResource: schema.GroupVersionResource{
+ Group: "dapr.io",
+ Version: "v1alpha1",
+ Resource: "components",
+ },
+ Kind: "Component",
+ Labels: kuberneteskeys.MakeSelectorLabels(application, name),
+ }
+}
From bf98a6bdbc43c8bdef1b7c75f8f2eba73cd73df7 Mon Sep 17 00:00:00 2001
From: nithyatsu <98416062+nithyatsu@users.noreply.github.com>
Date: Fri, 8 Sep 2023 14:30:48 -0700
Subject: [PATCH 10/13] support servicePort different from containerPort
(#6234)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Add support for "port" in
```
ports: {
web: {
containerPort: 5000
port: 80 // optional: only needs to be set when a value different from containerPort is desired
protocol: 'TCP' // optional: defaults to TCP
scheme: 'http' // optional: used to build URLs, defaults to http or https based on port
}
}
```
ref: https://github.com/radius-project/radius/issues/3865
## 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).
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #issue_number
## Auto-generated summary
### ๐ค Generated by Copilot at 33de0af
### Summary
๐งช๐โจ
This pull request adds support for service port values for container
ports in the `corerp` package. It updates the `containerPorts` struct
and the `generateService` function to render the `Port` and `TargetPort`
fields in the service spec. It also modifies a test function in
`render_test.go` to cover this feature.
> _Sing, O Muse, of the skillful coder who devised_
> _A clever way to render service ports for containers_
> _And tested well his work with inputs and outputs wise_
> _To please the gods of Kubernetes, the cloud maintainers._
### Walkthrough
* Fix typo in error message for invalid ports definition
([link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636L186-R197))
* Add service port values to container ports struct and append them from
input map
([link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636L245-R252),
[link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636R259),
[link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636L275-R284))
* Use service port values instead of container port values for service
port field in `generateService` function and add debug print
([link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-3da981bf20df07918a646eafcd867d6c95abac1f3aef1ed1b50a29f3f4cc8636L288-R299))
* Update test values and expected output for container port and service
port in `Test_DNS_Service_Generation` function in `render_test.go`
([link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94L1522-R1523),
[link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94R1534),
[link](https://github.com/radius-project/radius/pull/6234/files?diff=unified&w=0#diff-6f1219f263ab06a1493ac76960e2ab8cbe647e83e926bff7d13988f393334f94L1549-R1552))
---
pkg/corerp/renderers/container/render.go | 44 ++++++++-----------
pkg/corerp/renderers/container/render_test.go | 8 ++--
2 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/pkg/corerp/renderers/container/render.go b/pkg/corerp/renderers/container/render.go
index a680a81af8..e93338f3c6 100644
--- a/pkg/corerp/renderers/container/render.go
+++ b/pkg/corerp/renderers/container/render.go
@@ -183,11 +183,16 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
}
}
- for _, port := range properties.Container.Ports {
+ for portName, port := range properties.Container.Ports {
// if the container has an exposed port, note that down.
// A single service will be generated for a container with one or more exposed ports.
if port.ContainerPort == 0 {
- return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid ports definition: must define a ContainerPort, but ContainerPort was: %d.", port.ContainerPort))
+ return renderers.RendererOutput{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid ports definition: must define a ContainerPort, but ContainerPort is: %d.", port.ContainerPort))
+ }
+
+ if port.Port == 0 {
+ port.Port = port.ContainerPort
+ properties.Container.Ports[portName] = port
}
// if the container has an exposed port, but no 'provides' field, it requires DNS service generation.
@@ -239,26 +244,27 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
outputResources = append(outputResources, r.makeSecret(ctx, *resource, appId.Name(), secretData, options))
}
+ var servicePorts []corev1.ServicePort
+
// If the container has an exposed port and uses DNS-SD, generate a service for it.
if needsServiceGeneration {
- containerPorts := containerPorts{
- values: []int32{},
- names: []string{},
- }
-
for portName, port := range resource.Properties.Container.Ports {
// store portNames and portValues for use in service generation.
- containerPorts.names = append(containerPorts.names, portName)
- containerPorts.values = append(containerPorts.values, port.ContainerPort)
+ servicePort := corev1.ServicePort{
+ Name: portName,
+ Port: port.Port,
+ TargetPort: intstr.FromInt(int(port.ContainerPort)),
+ Protocol: corev1.ProtocolTCP,
+ }
+ servicePorts = append(servicePorts, servicePort)
}
// if a container has an exposed port, then we need to create a service for it.
basesrv := getServiceBase(baseManifest, appId.Name(), resource, &options)
- serviceResource, err := r.makeService(basesrv, resource, options, ctx, containerPorts)
+ serviceResource, err := r.makeService(basesrv, resource, options, ctx, servicePorts)
if err != nil {
return renderers.RendererOutput{}, err
}
-
outputResources = append(outputResources, serviceResource)
}
@@ -271,12 +277,7 @@ func (r Renderer) Render(ctx context.Context, dm v1.DataModelInterface, options
}, nil
}
-type containerPorts struct {
- values []int32
- names []string
-}
-
-func (r Renderer) makeService(base *corev1.Service, resource *datamodel.ContainerResource, options renderers.RenderOptions, ctx context.Context, containerPorts containerPorts) (rpv1.OutputResource, error) {
+func (r Renderer) makeService(base *corev1.Service, resource *datamodel.ContainerResource, options renderers.RenderOptions, ctx context.Context, servicePorts []corev1.ServicePort) (rpv1.OutputResource, error) {
appId, err := resources.ParseResource(resource.Properties.Application)
if err != nil {
return rpv1.OutputResource{}, v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid application id: %s. id: %s", err.Error(), resource.Properties.Application))
@@ -284,14 +285,7 @@ func (r Renderer) makeService(base *corev1.Service, resource *datamodel.Containe
// Ensure that we don't have any duplicate ports.
SKIPINSERT:
- for i, port := range containerPorts.values {
- newPort := corev1.ServicePort{
- Name: containerPorts.names[i],
- Port: port,
- TargetPort: intstr.FromInt(int(containerPorts.values[i])),
- Protocol: corev1.ProtocolTCP,
- }
-
+ for _, newPort := range servicePorts {
// Skip to add new port. Instead, upsert port if it already exists.
for j, p := range base.Spec.Ports {
if strings.EqualFold(p.Name, newPort.Name) || p.Port == newPort.Port || p.TargetPort.IntVal == newPort.TargetPort.IntVal {
diff --git a/pkg/corerp/renderers/container/render_test.go b/pkg/corerp/renderers/container/render_test.go
index de3673af7a..6bb274415d 100644
--- a/pkg/corerp/renderers/container/render_test.go
+++ b/pkg/corerp/renderers/container/render_test.go
@@ -1538,7 +1538,8 @@ func Test_ParseURL(t *testing.T) {
}
func Test_DNS_Service_Generation(t *testing.T) {
- var containerPortNumber int32 = 80
+ var containerPortNumber int32 = 3000
+ var servicePortNumber int32 = 80
t.Run("verify service generation", func(t *testing.T) {
properties := datamodel.ContainerProperties{
BasicResourceProperties: rpv1.BasicResourceProperties{
@@ -1549,6 +1550,7 @@ func Test_DNS_Service_Generation(t *testing.T) {
Ports: map[string]datamodel.ContainerPort{
"web": {
ContainerPort: int32(containerPortNumber),
+ Port: int32(servicePortNumber),
},
},
},
@@ -1565,8 +1567,8 @@ func Test_DNS_Service_Generation(t *testing.T) {
expectedServicePort := corev1.ServicePort{
Name: "web",
- Port: containerPortNumber,
- TargetPort: intstr.FromInt(80),
+ Port: 80,
+ TargetPort: intstr.FromInt(int(containerPortNumber)),
Protocol: "TCP",
}
From aeb267d0f50e89a98b3ca9c41644ab75e7c08241 Mon Sep 17 00:00:00 2001
From: Lakshmi Javadekar <103459615+lakshmimsft@users.noreply.github.com>
Date: Fri, 8 Sep 2023 15:14:08 -0700
Subject: [PATCH 11/13] Move Dapr test files under daprrp/resources (#6240)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Dapr _test.go files are under daprrp package instead of the
daprrp/resources location. Moving these files so they are consistent
with the other rp functional test structures.
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
## Auto-generated summary
### ๐ค Generated by Copilot at 3a3aa6d
### Summary
๐ง๐๐
Moved and renamed test files and bicep files for Dapr resources to a new
subdirectory and updated the references in the test code. This makes the
test code more organized and consistent with the naming convention for
Dapr resources.
### Walkthrough
* Updated template paths to reflect new location of bicep files after
moving test files to resources subdirectory
([link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-62351f7dd4881272041611c39fd5f275ac255529fe3ff99143232ae376e23605L29-R29),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-ff5eda5a82d6f745eb41d6b3c9fa5c8905ddf5a53afb464c04ae69b363186d6cL31-R31),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-ff5eda5a82d6f745eb41d6b3c9fa5c8905ddf5a53afb464c04ae69b363186d6cL83-R83),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-83abcb90756fde07a7163fb12a1c3418edd97afae0e6803e5d91fc72529db36dL30-R30),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-83abcb90756fde07a7163fb12a1c3418edd97afae0e6803e5d91fc72529db36dL79-R79),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-2b9e836ebf4b278de63298cd34643b88f6e6a9b4330daf2d1ba23967bb989b6dL29-R29),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-76b95c6cdc2b6c63edfb330450c6fe417de962651ae89502e88af3b31555272aL31-R31),
[link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-76b95c6cdc2b6c63edfb330450c6fe417de962651ae89502e88af3b31555272aL85-R85))
* Renamed `common.go` to `resources/common.go` to match the new file
structure
([link](https://github.com/radius-project/radius/pull/6240/files?diff=unified&w=0#diff-35782514faac29840c6692df8527f2d1b3717c78aaa5409bee25c72590a9a0fd))
---
test/functional/daprrp/{ => resources}/common.go | 0
.../{ => resources}/dapr_component_name_conflict_test.go | 2 +-
test/functional/daprrp/{ => resources}/dapr_pubsub_test.go | 4 ++--
.../daprrp/{ => resources}/dapr_secretstore_test.go | 4 ++--
.../daprrp/{ => resources}/dapr_serviceinvocation_test.go | 2 +-
.../functional/daprrp/{ => resources}/dapr_statestore_test.go | 4 ++--
6 files changed, 8 insertions(+), 8 deletions(-)
rename test/functional/daprrp/{ => resources}/common.go (100%)
rename test/functional/daprrp/{ => resources}/dapr_component_name_conflict_test.go (95%)
rename test/functional/daprrp/{ => resources}/dapr_pubsub_test.go (96%)
rename test/functional/daprrp/{ => resources}/dapr_secretstore_test.go (96%)
rename test/functional/daprrp/{ => resources}/dapr_serviceinvocation_test.go (96%)
rename test/functional/daprrp/{ => resources}/dapr_statestore_test.go (96%)
diff --git a/test/functional/daprrp/common.go b/test/functional/daprrp/resources/common.go
similarity index 100%
rename from test/functional/daprrp/common.go
rename to test/functional/daprrp/resources/common.go
diff --git a/test/functional/daprrp/dapr_component_name_conflict_test.go b/test/functional/daprrp/resources/dapr_component_name_conflict_test.go
similarity index 95%
rename from test/functional/daprrp/dapr_component_name_conflict_test.go
rename to test/functional/daprrp/resources/dapr_component_name_conflict_test.go
index b8f396552d..acdd7049a2 100644
--- a/test/functional/daprrp/dapr_component_name_conflict_test.go
+++ b/test/functional/daprrp/resources/dapr_component_name_conflict_test.go
@@ -26,7 +26,7 @@ import (
)
func Test_DaprComponentNameConflict(t *testing.T) {
- template := "resources/testdata/daprrp-resources-component-name-conflict.bicep"
+ template := "testdata/daprrp-resources-component-name-conflict.bicep"
name := "daprrp-rs-component-name-conflict"
validate := step.ValidateSingleDetail("DeploymentFailed", step.DeploymentErrorDetail{
diff --git a/test/functional/daprrp/dapr_pubsub_test.go b/test/functional/daprrp/resources/dapr_pubsub_test.go
similarity index 96%
rename from test/functional/daprrp/dapr_pubsub_test.go
rename to test/functional/daprrp/resources/dapr_pubsub_test.go
index e06d2664bb..f53b039e5e 100644
--- a/test/functional/daprrp/dapr_pubsub_test.go
+++ b/test/functional/daprrp/resources/dapr_pubsub_test.go
@@ -28,7 +28,7 @@ import (
)
func Test_DaprPubSubBroker_Manual(t *testing.T) {
- template := "resources/testdata/daprrp-resources-pubsub-broker-manual.bicep"
+ template := "testdata/daprrp-resources-pubsub-broker-manual.bicep"
name := "dpsb-manual-app"
appNamespace := "default-dpsb-manual-app"
@@ -80,7 +80,7 @@ func Test_DaprPubSubBroker_Manual(t *testing.T) {
}
func Test_DaprPubSubBroker_Recipe(t *testing.T) {
- template := "resources/testdata/daprrp-resources-pubsub-broker-recipe.bicep"
+ template := "testdata/daprrp-resources-pubsub-broker-recipe.bicep"
name := "dpsb-recipe-app"
appNamespace := "dpsb-recipe-env"
diff --git a/test/functional/daprrp/dapr_secretstore_test.go b/test/functional/daprrp/resources/dapr_secretstore_test.go
similarity index 96%
rename from test/functional/daprrp/dapr_secretstore_test.go
rename to test/functional/daprrp/resources/dapr_secretstore_test.go
index 490b182374..a7ac14ed7a 100644
--- a/test/functional/daprrp/dapr_secretstore_test.go
+++ b/test/functional/daprrp/resources/dapr_secretstore_test.go
@@ -27,7 +27,7 @@ import (
)
func Test_DaprSecretStore_Manual(t *testing.T) {
- template := "resources/testdata/daprrp-resources-secretstore-manual.bicep"
+ template := "testdata/daprrp-resources-secretstore-manual.bicep"
name := "daprrp-rs-secretstore-manual"
appNamespace := "default-daprrp-rs-secretstore-manual"
@@ -76,7 +76,7 @@ func Test_DaprSecretStore_Manual(t *testing.T) {
}
func Test_DaprSecretStore_Recipe(t *testing.T) {
- template := "resources/testdata/daprrp-resources-secretstore-recipe.bicep"
+ template := "testdata/daprrp-resources-secretstore-recipe.bicep"
name := "daprrp-rs-secretstore-recipe"
appNamespace := "daprrp-rs-secretstore-recipe"
diff --git a/test/functional/daprrp/dapr_serviceinvocation_test.go b/test/functional/daprrp/resources/dapr_serviceinvocation_test.go
similarity index 96%
rename from test/functional/daprrp/dapr_serviceinvocation_test.go
rename to test/functional/daprrp/resources/dapr_serviceinvocation_test.go
index 0bea322e1a..b20f663922 100644
--- a/test/functional/daprrp/dapr_serviceinvocation_test.go
+++ b/test/functional/daprrp/resources/dapr_serviceinvocation_test.go
@@ -26,7 +26,7 @@ import (
)
func Test_DaprServiceInvocation(t *testing.T) {
- template := "resources/testdata/daprrp-resources-serviceinvocation.bicep"
+ template := "testdata/daprrp-resources-serviceinvocation.bicep"
name := "dapr-serviceinvocation"
appNamespace := "default-dapr-serviceinvocation"
diff --git a/test/functional/daprrp/dapr_statestore_test.go b/test/functional/daprrp/resources/dapr_statestore_test.go
similarity index 96%
rename from test/functional/daprrp/dapr_statestore_test.go
rename to test/functional/daprrp/resources/dapr_statestore_test.go
index 73efcb73d3..50b80d1e5b 100644
--- a/test/functional/daprrp/dapr_statestore_test.go
+++ b/test/functional/daprrp/resources/dapr_statestore_test.go
@@ -28,7 +28,7 @@ import (
)
func Test_DaprStateStore_Manual(t *testing.T) {
- template := "resources/testdata/daprrp-resources-statestore-manual.bicep"
+ template := "testdata/daprrp-resources-statestore-manual.bicep"
name := "daprrp-rs-statestore-manual"
appNamespace := "default-daprrp-rs-statestore-manual"
@@ -82,7 +82,7 @@ func Test_DaprStateStore_Manual(t *testing.T) {
}
func Test_DaprStateStore_Recipe(t *testing.T) {
- template := "resources/testdata/daprrp-resources-statestore-recipe.bicep"
+ template := "testdata/daprrp-resources-statestore-recipe.bicep"
name := "daprrp-rs-sts-recipe"
appNamespace := "daprrp-env-recipes-env"
From e58dbc7a05901e4026afefd1a31d25a6c37b80b5 Mon Sep 17 00:00:00 2001
From: Yetkin Timocin
Date: Fri, 8 Sep 2023 17:25:51 -0700
Subject: [PATCH 12/13] Adding error codes as attributes to the Recipe Engine
and Driver metrics (#6205)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Adding error codes as attributes to the Recipe Engine and Driver metrics
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #issue_number
## Auto-generated summary
### ๐ค Generated by Copilot at c046059
### Summary
๐๐๐
This pull request improves the error handling and reporting in the
`recipes` package by using consistent and descriptive error codes and
metrics. It also updates the test cases and the `bicep` and `terraform`
drivers to use the new error codes and metrics.
> _`engine.go` changes_
> _better errors and metrics_
> _autumn of bugs ends_
### Walkthrough
* Add more descriptive and consistent error codes for recipe failures
([link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-4811c2c3581e1effba7ad029717fa82713a8e3a58b848b3845cfaf7d6a44fbacR37-R42),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-06702c6df95f9efe532b70614e1341244f575e5cbc030db57af268ecbfcd8c59L72-R79),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-06702c6df95f9efe532b70614e1341244f575e5cbc030db57af268ecbfcd8c59L117-R126),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-3fdb9931c1f2ddfc5b14d87f47b4124f2bdc1da187219fa1ad9cbe79fc326528L276-R276),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-3fdb9931c1f2ddfc5b14d87f47b4124f2bdc1da187219fa1ad9cbe79fc326528L438-R438))
* Use error codes from `GetRecipeErrorDetails` to record metrics in
`engine.go`
([link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-06702c6df95f9efe532b70614e1341244f575e5cbc030db57af268ecbfcd8c59L58-R64),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-06702c6df95f9efe532b70614e1341244f575e5cbc030db57af268ecbfcd8c59L103-R111))
* Use `RecipeDownloadFailed` instead of `FailedOperationState` to record
recipe download metrics in `bicep.go` and `execute.go`
([link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-f12e37706fa75e9e04b8dfa01a45cf9bf2a0463e1962605b2420d0cb343922a0L81-R81),
[link](https://github.com/radius-project/radius/pull/6205/files?diff=unified&w=0#diff-41ec31c0ceb8bd50c0329b55a8d27aeba69648059faef4e785302155f4e97482L295-R295))
---
grafana/radius-resource-provider-dashboard.json | 2 +-
pkg/recipes/driver/bicep.go | 2 +-
pkg/recipes/engine/engine.go | 6 ++++++
pkg/recipes/terraform/execute.go | 2 +-
4 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/grafana/radius-resource-provider-dashboard.json b/grafana/radius-resource-provider-dashboard.json
index 0fa27e6c99..334ac2d942 100644
--- a/grafana/radius-resource-provider-dashboard.json
+++ b/grafana/radius-resource-provider-dashboard.json
@@ -1922,7 +1922,7 @@
},
"editorMode": "code",
"exemplar": false,
- "expr": "(sum(rate(recipe_operation_duration_count{operation_state=\"failed\"}[$__rate_interval])) by (recipe_template_path, operation_type)) / (sum(rate(recipe_operation_duration_count[$__rate_interval])) by (recipe_template_path, operation_type))",
+ "expr": "(sum(rate(recipe_operation_duration_count{operation_state!=\"success\"}[$__rate_interval])) by (recipe_template_path, operation_type)) / (sum(rate(recipe_operation_duration_count[$__rate_interval])) by (recipe_template_path, operation_type))",
"format": "time_series",
"instant": false,
"interval": "",
diff --git a/pkg/recipes/driver/bicep.go b/pkg/recipes/driver/bicep.go
index c110194a7b..45fde7c048 100644
--- a/pkg/recipes/driver/bicep.go
+++ b/pkg/recipes/driver/bicep.go
@@ -78,7 +78,7 @@ func (d *bicepDriver) Execute(ctx context.Context, opts ExecuteOptions) (*recipe
err := util.ReadFromRegistry(ctx, opts.Definition.TemplatePath, &recipeData)
if err != nil {
metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime,
- metrics.NewRecipeAttributes(metrics.RecipeEngineOperationDownloadRecipe, opts.Recipe.Name, &opts.Definition, metrics.FailedOperationState))
+ metrics.NewRecipeAttributes(metrics.RecipeEngineOperationDownloadRecipe, opts.Recipe.Name, &opts.Definition, recipes.RecipeDownloadFailed))
return nil, recipes.NewRecipeError(recipes.RecipeDownloadFailed, err.Error(), recipes.GetRecipeErrorDetails(err))
}
metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime,
diff --git a/pkg/recipes/engine/engine.go b/pkg/recipes/engine/engine.go
index 658b4b544b..100b58b7bd 100644
--- a/pkg/recipes/engine/engine.go
+++ b/pkg/recipes/engine/engine.go
@@ -55,6 +55,9 @@ func (e *engine) Execute(ctx context.Context, recipe recipes.ResourceMetadata, p
recipeOutput, definition, err := e.executeCore(ctx, recipe, prevState)
if err != nil {
result = metrics.FailedOperationState
+ if recipes.GetRecipeErrorDetails(err) != nil {
+ result = recipes.GetRecipeErrorDetails(err).Code
+ }
}
metrics.DefaultRecipeEngineMetrics.RecordRecipeOperationDuration(ctx, executionStart,
@@ -100,6 +103,9 @@ func (e *engine) Delete(ctx context.Context, recipe recipes.ResourceMetadata, ou
definition, err := e.deleteCore(ctx, recipe, outputResources)
if err != nil {
result = metrics.FailedOperationState
+ if recipes.GetRecipeErrorDetails(err) != nil {
+ result = recipes.GetRecipeErrorDetails(err).Code
+ }
}
metrics.DefaultRecipeEngineMetrics.RecordRecipeOperationDuration(ctx, deletionStart,
diff --git a/pkg/recipes/terraform/execute.go b/pkg/recipes/terraform/execute.go
index 59075e5707..234df5ee71 100644
--- a/pkg/recipes/terraform/execute.go
+++ b/pkg/recipes/terraform/execute.go
@@ -292,7 +292,7 @@ func downloadAndInspect(ctx context.Context, workingDir string, execPath string,
if err := downloadModule(ctx, workingDir, execPath); err != nil {
metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime,
metrics.NewRecipeAttributes(metrics.RecipeEngineOperationDownloadRecipe, options.EnvRecipe.Name,
- options.EnvRecipe, metrics.FailedOperationState))
+ options.EnvRecipe, recipes.RecipeDownloadFailed))
return nil, recipes.NewRecipeError(recipes.RecipeDownloadFailed, err.Error(), recipes.GetRecipeErrorDetails(err))
}
metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime,
From 2606ade7e2e0c687891629a76e943f7694503383 Mon Sep 17 00:00:00 2001
From: Young Bu Park
Date: Sat, 9 Sep 2023 19:33:31 -0700
Subject: [PATCH 13/13] Simplify API route registration (#5851)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
* Goal: This PR introduces new abstraction layer to register frontend
and backend controllers in the single place.
* The abstraction layer is applied to only Core RP.
## Type of change
- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).
Fixes: #6207
## Auto-generated summary
### ๐ค Generated by Copilot at 012d647
### Summary
๐๐ ๏ธ๐
This pull request adds the core functionality and configuration for the
radius resource provider service. It implements the data model
converters, the main entry point, the service configuration files, the
API and async worker services, and the resource type handlers and
converters for the environments and containers resources.
> _`radius` service_
> _configures and converts types_
> _spring cleaning code_
### Walkthrough
* Add the main.go file for the radius resource provider service
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-fa735479e022c6e49848540a0b9903622e3262f095d616e14bb7c04e50ebcab1R1-R132))
* Add the service configuration files for different environments
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-07e9a95e043dfbe94e4f390db189581952fd2b807d6661c30532c37e12f66ac8R1-R50),
[link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-891f6c038d9a9192d8a42aa6f3a3a2256a23e5a0bea53e01e0bbc1fa185cb7f1R1-R37),
[link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-17c9124727ad9ae141034516dae9a112fa19d262c8ed4a9cba43941394db0e2fR1-R41))
* Add the types and interfaces for the resource nodes, handlers, and
converters
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-aabe1821e79e59f4024d20a4c4a12044b00234673d3056f1b4751f7e2dbab7a4R1-R125))
* Add the setup.go file for setting up the namespace and resource type
handlers
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-c6b4dcdeb10a61179403c951a5128398c966e787eb8a429fd225527300c3275cR1-R74))
* Add the apiservice.go file for the API service of the service
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-4236d50bf5038163c457c84cd68eea9c4d6f58d9f3d74de4ff7662b823cc32afR1-R77))
* Add the asyncworker.go file for the async worker service of the
service
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-0d8522466cd60aa335c74f53b18be7dfa7ba7cf052d3cd8deb44b3d08646af6bR1-R114))
* Add the wrapper functions for converting the container data model to
and from the versioned model
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-595d43d19193d49cdcb642ef52b30c4a22a60bdb9714f0749cf1ff7b9a49990bR27-R30),
[link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-595d43d19193d49cdcb642ef52b30c4a22a60bdb9714f0749cf1ff7b9a49990bR44-R47))
* Add the wrapper functions for converting the environment data model to
and from the versioned model
([link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-24beaa42c10938adfeb96b798c5f16db5002b40b319d86d2080157b0b95e4d5bR28-R32),
[link](https://github.com/project-radius/radius/pull/5851/files?diff=unified&w=0#diff-24beaa42c10938adfeb96b798c5f16db5002b40b319d86d2080157b0b95e4d5bR48-R52))
Co-authored-by: Ryan Nowak
---
cmd/applications-rp/main.go | 114 ++--
cmd/applications-rp/portableresource-dev.yaml | 1 +
.../portableresource-self-hosted.yaml | 1 +
cmd/applications-rp/radius-cloud.yaml | 1 +
cmd/applications-rp/radius-dev.yaml | 1 +
cmd/applications-rp/radius-self-hosted.yaml | 1 +
cmd/ucpd/ucp-self-hosted-dev.yaml | 1 +
deploy/Chart/templates/rp/configmaps.yaml | 2 +
deploy/Chart/templates/rp/deployment.yaml | 1 -
deploy/Chart/templates/ucp/configmaps.yaml | 6 +-
.../configSettings.md | 1 +
pkg/armrpc/api/v1/types.go | 34 +-
.../statusmanager/statusmanager.go | 51 +-
.../statusmanager/statusmanager_test.go | 17 +-
pkg/armrpc/asyncoperation/worker/registry.go | 2 +-
pkg/armrpc/asyncoperation/worker/service.go | 52 +-
pkg/armrpc/builder/builder.go | 184 +++++++
pkg/armrpc/builder/builder_test.go | 267 +++++++++
pkg/armrpc/builder/namespace.go | 84 +++
pkg/armrpc/builder/namespace_test.go | 445 +++++++++++++++
pkg/armrpc/builder/node.go | 73 +++
pkg/armrpc/builder/node_test.go | 47 ++
pkg/armrpc/builder/operation.go | 367 +++++++++++++
pkg/armrpc/builder/operation_test.go | 512 ++++++++++++++++++
pkg/armrpc/builder/types.go | 82 +++
pkg/armrpc/frontend/controller/controller.go | 2 +-
.../getavailableoperations.go | 50 ++
.../defaultoperation/getoperationresult.go | 18 +-
.../getoperationresult_test.go | 49 +-
.../testdata/operationstatus_datamodel.json | 4 +-
.../testdata/operationstatus_output.json | 4 +-
pkg/armrpc/frontend/server/handler.go | 32 +-
pkg/armrpc/frontend/server/handler_test.go | 28 +
pkg/armrpc/frontend/server/server.go | 18 +-
pkg/armrpc/frontend/server/service.go | 9 +-
pkg/armrpc/rpctest/routers.go | 69 ++-
pkg/armrpc/rpctest/testdatamodel.go | 166 ++++++
pkg/corerp/backend/service.go | 20 +-
.../frontend/handler/getoperations_test.go | 78 ---
pkg/corerp/frontend/handler/routes_test.go | 4 +-
pkg/corerp/frontend/service.go | 98 ----
pkg/corerp/setup/operations.go | 262 +++++++++
pkg/corerp/setup/setup.go | 217 ++++++++
pkg/corerp/setup/setup_test.go | 253 +++++++++
pkg/kubeutil/client.go | 50 ++
pkg/middleware/logger.go | 24 +-
.../controller/createorupdateresource.go | 2 +-
.../controller/createorupdateresource_test.go | 43 +-
.../backend/controller/deleteresource.go | 2 +-
.../backend/controller/deleteresource_test.go | 2 +-
pkg/portableresources/backend/service.go | 82 ++-
.../frontend/handler/routes_test.go | 8 +-
pkg/portableresources/frontend/service.go | 8 +-
pkg/recipes/controllerconfig/config.go | 85 +++
pkg/server/apiservice.go | 89 +++
pkg/server/asyncworker.go | 97 ++++
pkg/ucp/frontend/api/server.go | 4 +-
pkg/ucp/frontend/aws/module.go | 8 +
pkg/ucp/frontend/aws/routes.go | 8 +-
pkg/ucp/frontend/aws/routes_test.go | 4 +-
pkg/ucp/integrationtests/testrp/async.go | 5 +-
.../integrationtests/testserver/testserver.go | 5 +-
pkg/ucp/queue/provider/factory.go | 10 +-
pkg/ucp/queue/provider/options.go | 3 +
pkg/ucp/queue/provider/provider.go | 7 +-
pkg/ucp/queue/provider/provider_test.go | 6 +-
pkg/ucp/server/server.go | 26 +-
67 files changed, 3756 insertions(+), 550 deletions(-)
create mode 100644 pkg/armrpc/builder/builder.go
create mode 100644 pkg/armrpc/builder/builder_test.go
create mode 100644 pkg/armrpc/builder/namespace.go
create mode 100644 pkg/armrpc/builder/namespace_test.go
create mode 100644 pkg/armrpc/builder/node.go
create mode 100644 pkg/armrpc/builder/node_test.go
create mode 100644 pkg/armrpc/builder/operation.go
create mode 100644 pkg/armrpc/builder/operation_test.go
create mode 100644 pkg/armrpc/builder/types.go
create mode 100644 pkg/armrpc/frontend/defaultoperation/getavailableoperations.go
create mode 100644 pkg/armrpc/rpctest/testdatamodel.go
delete mode 100644 pkg/corerp/frontend/handler/getoperations_test.go
delete mode 100644 pkg/corerp/frontend/service.go
create mode 100644 pkg/corerp/setup/operations.go
create mode 100644 pkg/corerp/setup/setup.go
create mode 100644 pkg/corerp/setup/setup_test.go
create mode 100644 pkg/recipes/controllerconfig/config.go
create mode 100644 pkg/server/apiservice.go
create mode 100644 pkg/server/asyncworker.go
diff --git a/cmd/applications-rp/main.go b/cmd/applications-rp/main.go
index 076930b086..57b93a00a0 100644
--- a/cmd/applications-rp/main.go
+++ b/cmd/applications-rp/main.go
@@ -18,21 +18,25 @@ package main
import (
"context"
- "flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
+ "github.com/go-logr/logr"
+ "github.com/spf13/pflag"
+ etcdclient "go.etcd.io/etcd/client/v3"
+ runtimelog "sigs.k8s.io/controller-runtime/pkg/log"
+
+ "github.com/radius-project/radius/pkg/armrpc/builder"
"github.com/radius-project/radius/pkg/armrpc/hostoptions"
- "github.com/radius-project/radius/pkg/corerp/backend"
- "github.com/radius-project/radius/pkg/corerp/frontend"
metricsservice "github.com/radius-project/radius/pkg/metrics/service"
profilerservice "github.com/radius-project/radius/pkg/profiler/service"
+ "github.com/radius-project/radius/pkg/recipes/controllerconfig"
+ "github.com/radius-project/radius/pkg/server"
"github.com/radius-project/radius/pkg/trace"
- "github.com/radius-project/radius/pkg/logging"
pr_backend "github.com/radius-project/radius/pkg/portableresources/backend"
pr_frontend "github.com/radius-project/radius/pkg/portableresources/frontend"
"github.com/radius-project/radius/pkg/ucp/data"
@@ -40,53 +44,31 @@ import (
"github.com/radius-project/radius/pkg/ucp/hosting"
"github.com/radius-project/radius/pkg/ucp/ucplog"
- "github.com/go-logr/logr"
- etcdclient "go.etcd.io/etcd/client/v3"
- runtimelog "sigs.k8s.io/controller-runtime/pkg/log"
+ corerp_setup "github.com/radius-project/radius/pkg/corerp/setup"
)
-const serviceName = "applications.core"
-
-func newPortableResourceHosts(configFile string, enableAsyncWorker bool) ([]hosting.Service, *hostoptions.HostOptions, error) {
- hostings := []hosting.Service{}
- options, err := hostoptions.NewHostOptionsFromEnvironment(configFile)
- if err != nil {
- return nil, nil, err
- }
- hostings = append(hostings, pr_frontend.NewService(options))
- if enableAsyncWorker {
- hostings = append(hostings, pr_backend.NewService(options))
- }
-
- return hostings, &options, nil
-}
+const serviceName = "radius"
func main() {
var configFile string
- var enableAsyncWorker bool
-
- var runPortableResource bool
- var portableResourceConfigFile string
-
defaultConfig := fmt.Sprintf("radius-%s.yaml", hostoptions.Environment())
- flag.StringVar(&configFile, "config-file", defaultConfig, "The service configuration file.")
- flag.BoolVar(&enableAsyncWorker, "enable-asyncworker", true, "Flag to run async request process worker (for private preview and dev/test purpose).")
-
- flag.BoolVar(&runPortableResource, "run-portableresource", true, "Flag to run portable resources RPs(for private preview and dev/test purpose).")
- defaultPortableRsConfig := fmt.Sprintf("portableresource-%s.yaml", hostoptions.Environment())
- flag.StringVar(&portableResourceConfigFile, "portableresource-config", defaultPortableRsConfig, "The service configuration file for portable resource providers.")
-
+ pflag.StringVar(&configFile, "config-file", defaultConfig, "The service configuration file.")
if configFile == "" {
log.Fatal("config-file is empty.") //nolint:forbidigo // this is OK inside the main function.
}
- flag.Parse()
+ var portableResourceConfigFile string
+ defaultPortableRsConfig := fmt.Sprintf("portableresource-%s.yaml", hostoptions.Environment())
+ pflag.StringVar(&portableResourceConfigFile, "portableresource-config", defaultPortableRsConfig, "The service configuration file for portable resource providers.")
+
+ pflag.Parse()
options, err := hostoptions.NewHostOptionsFromEnvironment(configFile)
if err != nil {
log.Fatal(err) //nolint:forbidigo // this is OK inside the main function.
}
- hostingSvc := []hosting.Service{frontend.NewService(options)}
+
+ hostingSvc := []hosting.Service{}
metricOptions := metricsservice.NewHostOptionsFromEnvironment(*options.Config)
metricOptions.Config.ServiceName = serviceName
@@ -99,7 +81,7 @@ func main() {
hostingSvc = append(hostingSvc, profilerservice.NewService(profilerOptions))
}
- logger, flush, err := ucplog.NewLogger(logging.AppCoreLoggerName, &options.Config.Logging)
+ logger, flush, err := ucplog.NewLogger(serviceName, &options.Config.Logging)
if err != nil {
log.Fatal(err) //nolint:forbidigo // this is OK inside the main function.
}
@@ -108,22 +90,10 @@ func main() {
// Must set the logger before using controller-runtime.
runtimelog.SetLogger(logger)
- if enableAsyncWorker {
- logger.Info("Enable AsyncRequestProcessWorker.")
- hostingSvc = append(hostingSvc, backend.NewService(options))
- }
-
- // Configure Portable Resources to run it with Applications.Core RP.
- var portableResourceOpts *hostoptions.HostOptions
- if runPortableResource && portableResourceConfigFile != "" {
- logger.Info("Run Service for Portable Resource Providers.")
- var portableResourceSvcs []hosting.Service
- var err error
- portableResourceSvcs, portableResourceOpts, err = newPortableResourceHosts(portableResourceConfigFile, enableAsyncWorker)
- if err != nil {
- log.Fatal(err) //nolint:forbidigo // this is OK inside the main function.
- }
- hostingSvc = append(hostingSvc, portableResourceSvcs...)
+ // Load portable resource config.
+ prOptions, err := hostoptions.NewHostOptionsFromEnvironment(portableResourceConfigFile)
+ if err != nil {
+ log.Fatal(err) //nolint:forbidigo // this is OK inside the main function.
}
if options.Config.StorageProvider.Provider == dataprovider.TypeETCD &&
@@ -135,13 +105,31 @@ func main() {
client := hosting.NewAsyncValue[etcdclient.Client]()
options.Config.StorageProvider.ETCD.Client = client
options.Config.SecretProvider.ETCD.Client = client
- if portableResourceOpts != nil {
- portableResourceOpts.Config.StorageProvider.ETCD.Client = client
- portableResourceOpts.Config.SecretProvider.ETCD.Client = client
- }
+
+ // Portable resource options
+ prOptions.Config.StorageProvider.ETCD.Client = client
+ prOptions.Config.SecretProvider.ETCD.Client = client
+
hostingSvc = append(hostingSvc, data.NewEmbeddedETCDService(data.EmbeddedETCDServiceOptions{ClientConfigSink: client}))
}
+ builders, err := builders(options)
+ if err != nil {
+ log.Fatal(err) //nolint:forbidigo // this is OK inside the main function.
+ }
+
+ hostingSvc = append(
+ hostingSvc,
+ server.NewAPIService(options, builders),
+ server.NewAsyncWorker(options, builders),
+
+ // Configure Portable Resources to run it with Applications.Core RP.
+ //
+ // This is temporary until we migrate these resources to use the new registration model.
+ pr_frontend.NewService(prOptions),
+ pr_backend.NewService(prOptions),
+ )
+
loggerValues := []any{}
host := &hosting.Host{
Services: hostingSvc,
@@ -190,3 +178,15 @@ func main() {
panic(err)
}
}
+
+func builders(options hostoptions.HostOptions) ([]builder.Builder, error) {
+ config, err := controllerconfig.New(options)
+ if err != nil {
+ return nil, err
+ }
+
+ return []builder.Builder{
+ corerp_setup.SetupNamespace(config).GenerateBuilder(),
+ // Add resource provider builders...
+ }, nil
+}
diff --git a/cmd/applications-rp/portableresource-dev.yaml b/cmd/applications-rp/portableresource-dev.yaml
index 77c5a933b0..89fd579140 100644
--- a/cmd/applications-rp/portableresource-dev.yaml
+++ b/cmd/applications-rp/portableresource-dev.yaml
@@ -8,6 +8,7 @@ storageProvider:
inmemory: true
queueProvider:
provider: inmemory
+ name: radiusportable
profilerProvider:
enabled: true
port: 6060
diff --git a/cmd/applications-rp/portableresource-self-hosted.yaml b/cmd/applications-rp/portableresource-self-hosted.yaml
index fdadd10dda..37c587e086 100644
--- a/cmd/applications-rp/portableresource-self-hosted.yaml
+++ b/cmd/applications-rp/portableresource-self-hosted.yaml
@@ -17,6 +17,7 @@ storageProvider:
namespace: 'radius-testing'
queueProvider:
provider: "apiserver"
+ name: radiusportable
apiserver:
context: ''
namespace: 'radius-testing'
diff --git a/cmd/applications-rp/radius-cloud.yaml b/cmd/applications-rp/radius-cloud.yaml
index 8555e931fa..66c36258a2 100644
--- a/cmd/applications-rp/radius-cloud.yaml
+++ b/cmd/applications-rp/radius-cloud.yaml
@@ -19,6 +19,7 @@ storageProvider:
masterKey: set-me-in-a-different-way
queueProvider:
provider: inmemory
+ name: radius
profilerProvider:
enabled: true
port: 6060
diff --git a/cmd/applications-rp/radius-dev.yaml b/cmd/applications-rp/radius-dev.yaml
index a679384d92..3c64758fe6 100644
--- a/cmd/applications-rp/radius-dev.yaml
+++ b/cmd/applications-rp/radius-dev.yaml
@@ -8,6 +8,7 @@ storageProvider:
inmemory: true
queueProvider:
provider: inmemory
+ name: radius
profilerProvider:
enabled: true
port: 6060
diff --git a/cmd/applications-rp/radius-self-hosted.yaml b/cmd/applications-rp/radius-self-hosted.yaml
index 63090e1448..b247043a15 100644
--- a/cmd/applications-rp/radius-self-hosted.yaml
+++ b/cmd/applications-rp/radius-self-hosted.yaml
@@ -17,6 +17,7 @@ storageProvider:
namespace: 'radius-testing'
queueProvider:
provider: "apiserver"
+ name: radius
apiserver:
context: ''
namespace: 'radius-testing'
diff --git a/cmd/ucpd/ucp-self-hosted-dev.yaml b/cmd/ucpd/ucp-self-hosted-dev.yaml
index 5f14d8a49b..8e73d30861 100644
--- a/cmd/ucpd/ucp-self-hosted-dev.yaml
+++ b/cmd/ucpd/ucp-self-hosted-dev.yaml
@@ -20,6 +20,7 @@ secretProvider:
queueProvider:
provider: "apiserver"
+ name: 'ucp'
apiserver:
context: ''
namespace: 'radius-testing'
diff --git a/deploy/Chart/templates/rp/configmaps.yaml b/deploy/Chart/templates/rp/configmaps.yaml
index a66ed7437b..8dbecbc90e 100644
--- a/deploy/Chart/templates/rp/configmaps.yaml
+++ b/deploy/Chart/templates/rp/configmaps.yaml
@@ -20,6 +20,7 @@ data:
namespace: "radius-system"
queueProvider:
provider: "apiserver"
+ name: "radius"
apiserver:
context: ""
namespace: "radius-system"
@@ -66,6 +67,7 @@ data:
namespace: "radius-system"
queueProvider:
provider: "apiserver"
+ name: "radiusportable"
apiserver:
context: ""
namespace: "radius-system"
diff --git a/deploy/Chart/templates/rp/deployment.yaml b/deploy/Chart/templates/rp/deployment.yaml
index 3da0d8dbe4..d3ee662bd9 100644
--- a/deploy/Chart/templates/rp/deployment.yaml
+++ b/deploy/Chart/templates/rp/deployment.yaml
@@ -32,7 +32,6 @@ spec:
image: "{{ .Values.rp.image }}:{{ .Values.rp.tag | default $appversion }}"
args:
- --config-file=/etc/config/radius-self-host.yaml
- - --run-portableresource
- --portableresource-config=/etc/config/portableresource-self-host.yaml
env:
- name: SKIP_ARM
diff --git a/deploy/Chart/templates/ucp/configmaps.yaml b/deploy/Chart/templates/ucp/configmaps.yaml
index 7d9a8154cd..95b27be087 100644
--- a/deploy/Chart/templates/ucp/configmaps.yaml
+++ b/deploy/Chart/templates/ucp/configmaps.yaml
@@ -20,7 +20,11 @@ data:
provider: kubernetes
queueProvider:
- provider: inmemory
+ provider: "apiserver"
+ name: "ucp"
+ apiserver:
+ context: ""
+ namespace: "radius-system"
profilerProvider:
enabled: true
diff --git a/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md b/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md
index 3d8723dafe..2043051d22 100644
--- a/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md
+++ b/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md
@@ -197,6 +197,7 @@ storageProvider:
namespace: "radius-system"
queueProvider:
provider: "apiserver"
+ name: "radius"
apiserver:
context: ""
namespace: "radius-system"
diff --git a/pkg/armrpc/api/v1/types.go b/pkg/armrpc/api/v1/types.go
index 5e4ed75020..623a2fafbc 100644
--- a/pkg/armrpc/api/v1/types.go
+++ b/pkg/armrpc/api/v1/types.go
@@ -44,17 +44,15 @@ var (
type OperationMethod string
var operationMethodToHTTPMethod = map[OperationMethod]string{
- OperationList: http.MethodGet,
- OperationGet: http.MethodGet,
- OperationPut: http.MethodPut,
- OperationPatch: http.MethodPatch,
- OperationDelete: http.MethodDelete,
+ OperationPlaneScopeList: http.MethodGet,
+ OperationList: http.MethodGet,
+ OperationGet: http.MethodGet,
+ OperationPut: http.MethodPut,
+ OperationPatch: http.MethodPatch,
+ OperationDelete: http.MethodDelete,
// ARM RPC specific operations.
- OperationGetOperations: http.MethodGet,
- OperationGetOperationStatuses: http.MethodGet,
- OperationGetOperationResult: http.MethodGet,
- OperationPutSubscriptions: http.MethodPut,
+ OperationPutSubscriptions: http.MethodPut,
// Non-idempotent lifecycle operations.
OperationGetImperative: http.MethodPost,
@@ -80,16 +78,14 @@ func (o OperationMethod) HTTPMethod() string {
const (
// Predefined Operation methods.
- OperationList OperationMethod = "LIST"
- OperationGet OperationMethod = "GET"
- OperationPut OperationMethod = "PUT"
- OperationPatch OperationMethod = "PATCH"
- OperationDelete OperationMethod = "DELETE"
- OperationGetOperations OperationMethod = "GETOPERATIONS"
- OperationGetOperationStatuses OperationMethod = "GETOPERATIONSTATUSES"
- OperationGetOperationResult OperationMethod = "GETOPERATIONRESULT"
- OperationPutSubscriptions OperationMethod = "PUTSUBSCRIPTIONS"
- OperationPost OperationMethod = "POST"
+ OperationPlaneScopeList OperationMethod = "LISTPLANESCOPE"
+ OperationList OperationMethod = "LIST"
+ OperationGet OperationMethod = "GET"
+ OperationPut OperationMethod = "PUT"
+ OperationPatch OperationMethod = "PATCH"
+ OperationDelete OperationMethod = "DELETE"
+ OperationPutSubscriptions OperationMethod = "PUTSUBSCRIPTIONS"
+ OperationPost OperationMethod = "POST"
// Imperative operation methods for non-idempotent lifecycle operations.
// UCP extends the ARM resource lifecycle to support using POST for non-idempotent resource types.
diff --git a/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go b/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go
index 954324b755..a51525c2ca 100644
--- a/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go
+++ b/pkg/armrpc/asyncoperation/statusmanager/statusmanager.go
@@ -27,6 +27,7 @@ import (
ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
"github.com/radius-project/radius/pkg/metrics"
"github.com/radius-project/radius/pkg/trace"
+ "github.com/radius-project/radius/pkg/ucp/dataprovider"
queue "github.com/radius-project/radius/pkg/ucp/queue/client"
"github.com/radius-project/radius/pkg/ucp/resources"
"github.com/radius-project/radius/pkg/ucp/store"
@@ -36,10 +37,9 @@ import (
// statusManager includes the necessary functions to manage asynchronous operations.
type statusManager struct {
- storeClient store.StorageClient
- queue queue.Client
- providerName string
- location string
+ storeProvider dataprovider.DataStorageProvider
+ queue queue.Client
+ location string
}
// QueueOperationOptions is the options type provided when queueing an async operation.
@@ -65,12 +65,11 @@ type StatusManager interface {
}
// New creates statusManager instance.
-func New(storeClient store.StorageClient, q queue.Client, providerName, location string) StatusManager {
+func New(dataProvider dataprovider.DataStorageProvider, q queue.Client, location string) StatusManager {
return &statusManager{
- storeClient: storeClient,
- queue: q,
- providerName: providerName,
- location: location,
+ storeProvider: dataProvider,
+ queue: q,
+ location: location,
}
}
@@ -79,6 +78,10 @@ func (aom *statusManager) operationStatusResourceID(id resources.ID, operationID
return fmt.Sprintf("%s/providers/%s/locations/%s/operationstatuses/%s", id.PlaneScope(), strings.ToLower(id.ProviderNamespace()), aom.location, operationID)
}
+func (aom *statusManager) getClient(ctx context.Context, id resources.ID) (store.StorageClient, error) {
+ return aom.storeProvider.GetStorageClient(ctx, id.ProviderNamespace()+"/operationstatuses")
+}
+
// QueueAsyncOperation creates and saves a new status resource with the given parameters in datastore, and queues
// a request message. If an error occurs, the status is deleted using the storeClient.
func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMRequestContext, options QueueOperationOptions) error {
@@ -108,7 +111,12 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR
ClientObjectID: sCtx.ClientObjectID,
}
- err := aom.storeClient.Save(ctx, &store.Object{
+ storeClient, err := aom.getClient(ctx, sCtx.ResourceID)
+ if err != nil {
+ return err
+ }
+
+ err = storeClient.Save(ctx, &store.Object{
Metadata: store.Metadata{ID: opID},
Data: aos,
})
@@ -118,7 +126,7 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR
}
if err = aom.queueRequestMessage(ctx, sCtx, aos, options.OperationTimeout); err != nil {
- delErr := aom.storeClient.Delete(ctx, opID)
+ delErr := storeClient.Delete(ctx, opID)
if delErr != nil {
return delErr
}
@@ -132,7 +140,12 @@ func (aom *statusManager) QueueAsyncOperation(ctx context.Context, sCtx *v1.ARMR
// Get gets a status object from the datastore or an error if the retrieval fails.
func (aom *statusManager) Get(ctx context.Context, id resources.ID, operationID uuid.UUID) (*Status, error) {
- obj, err := aom.storeClient.Get(ctx, aom.operationStatusResourceID(id, operationID))
+ storeClient, err := aom.getClient(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ obj, err := storeClient.Get(ctx, aom.operationStatusResourceID(id, operationID))
if err != nil {
return nil, err
}
@@ -149,8 +162,12 @@ func (aom *statusManager) Get(ctx context.Context, id resources.ID, operationID
// given parameters, and saves it back to the store.
func (aom *statusManager) Update(ctx context.Context, id resources.ID, operationID uuid.UUID, state v1.ProvisioningState, endTime *time.Time, opError *v1.ErrorDetails) error {
opID := aom.operationStatusResourceID(id, operationID)
+ storeClient, err := aom.getClient(ctx, id)
+ if err != nil {
+ return err
+ }
- obj, err := aom.storeClient.Get(ctx, opID)
+ obj, err := storeClient.Get(ctx, opID)
if err != nil {
return err
}
@@ -173,13 +190,17 @@ func (aom *statusManager) Update(ctx context.Context, id resources.ID, operation
obj.Data = s
- return aom.storeClient.Save(ctx, obj, store.WithETag(obj.ETag))
+ return storeClient.Save(ctx, obj, store.WithETag(obj.ETag))
}
// Delete deletes the operation status resource associated with the given ID and
// operationID, and returns an error if unsuccessful.
func (aom *statusManager) Delete(ctx context.Context, id resources.ID, operationID uuid.UUID) error {
- return aom.storeClient.Delete(ctx, aom.operationStatusResourceID(id, operationID))
+ storeClient, err := aom.getClient(ctx, id)
+ if err != nil {
+ return err
+ }
+ return storeClient.Delete(ctx, aom.operationStatusResourceID(id, operationID))
}
// queueRequestMessage function is to put the async operation message to the queue to be worked on.
diff --git a/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go b/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go
index 8a3fa6b87a..43b88d0482 100644
--- a/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go
+++ b/pkg/armrpc/asyncoperation/statusmanager/statusmanager_test.go
@@ -26,6 +26,7 @@ import (
"github.com/google/uuid"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/radius-project/radius/pkg/ucp/dataprovider"
queue "github.com/radius-project/radius/pkg/ucp/queue/client"
"github.com/radius-project/radius/pkg/ucp/resources"
"github.com/radius-project/radius/pkg/ucp/store"
@@ -33,9 +34,10 @@ import (
)
type asyncOperationsManagerTest struct {
- manager StatusManager
- storeClient *store.MockStorageClient
- queue *queue.MockClient
+ manager StatusManager
+ storeProvider *dataprovider.MockDataStorageProvider
+ storeClient *store.MockStorageClient
+ queue *queue.MockClient
}
const (
@@ -51,13 +53,16 @@ const (
func setup(tb testing.TB) (asyncOperationsManagerTest, *gomock.Controller) {
ctrl := gomock.NewController(tb)
+ dp := dataprovider.NewMockDataStorageProvider(ctrl)
sc := store.NewMockStorageClient(ctrl)
+ dp.EXPECT().GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses").Return(sc, nil)
enq := queue.NewMockClient(ctrl)
- aom := New(sc, enq, "Test-AsyncOperationsManager", "test-location")
- return asyncOperationsManagerTest{manager: aom, storeClient: sc, queue: enq}, ctrl
+ aom := New(dp, enq, "test-location")
+ return asyncOperationsManagerTest{manager: aom, storeProvider: dp, storeClient: sc, queue: enq}, ctrl
}
var reqCtx = &v1.ARMRequestContext{
+ ResourceID: resources.MustParse("/planes/radius/local/resourceGroups/radius-test-rg/providers/Applications.Core/container/container0"),
OperationID: uuid.Must(uuid.NewRandom()),
HomeTenantID: "home-tenant-id",
ClientObjectID: "client-object-id",
@@ -99,7 +104,7 @@ func TestOperationStatusResourceID(t *testing.T) {
},
}
- sm := &statusManager{providerName: "applications.core", location: v1.LocationGlobal}
+ sm := &statusManager{location: v1.LocationGlobal}
for _, tc := range resourceIDTests {
t.Run(tc.resourceID, func(t *testing.T) {
diff --git a/pkg/armrpc/asyncoperation/worker/registry.go b/pkg/armrpc/asyncoperation/worker/registry.go
index 4a178e7b7f..814fc85b6d 100644
--- a/pkg/armrpc/asyncoperation/worker/registry.go
+++ b/pkg/armrpc/asyncoperation/worker/registry.go
@@ -49,7 +49,7 @@ func (h *ControllerRegistry) Register(ctx context.Context, resourceType string,
ot := v1.OperationType{Type: resourceType, Method: method}
- storageClient, err := opts.DataProvider.GetStorageClient(ctx, resourceType)
+ storageClient, err := h.sp.GetStorageClient(ctx, resourceType)
if err != nil {
return err
}
diff --git a/pkg/armrpc/asyncoperation/worker/service.go b/pkg/armrpc/asyncoperation/worker/service.go
index d2ce0cec69..4a04a19384 100644
--- a/pkg/armrpc/asyncoperation/worker/service.go
+++ b/pkg/armrpc/asyncoperation/worker/service.go
@@ -21,18 +21,10 @@ import (
manager "github.com/radius-project/radius/pkg/armrpc/asyncoperation/statusmanager"
"github.com/radius-project/radius/pkg/armrpc/hostoptions"
- "github.com/radius-project/radius/pkg/kubeutil"
"github.com/radius-project/radius/pkg/ucp/dataprovider"
queue "github.com/radius-project/radius/pkg/ucp/queue/client"
qprovider "github.com/radius-project/radius/pkg/ucp/queue/provider"
"github.com/radius-project/radius/pkg/ucp/ucplog"
-
- "k8s.io/client-go/discovery"
- "k8s.io/client-go/kubernetes"
-
- "k8s.io/client-go/dynamic"
-
- controller_runtime "sigs.k8s.io/controller-runtime/pkg/client"
)
// Service is the base worker service implementation to initialize and start worker.
@@ -49,58 +41,20 @@ type Service struct {
Controllers *ControllerRegistry
// RequestQueue is the queue client for async operation request message.
RequestQueue queue.Client
- // KubeClient is the Kubernetes controller runtime client.
- KubeClient controller_runtime.Client
- // KubeClientSet is the Kubernetes client.
- KubeClientSet kubernetes.Interface
- // KubeDiscoveryClient is the Kubernetes discovery client.
- KubeDiscoveryClient discovery.ServerResourcesInterface
- // KubeDynamicClientSet is the Kubernetes dynamic client.
- KubeDynamicClientSet dynamic.Interface
}
// Init initializes worker service - it initializes the StorageProvider, RequestQueue, OperationStatusManager, Controllers, KubeClient and
// returns an error if any of these operations fail.
func (s *Service) Init(ctx context.Context) error {
s.StorageProvider = dataprovider.NewStorageProvider(s.Options.Config.StorageProvider)
- qp := qprovider.New(s.ProviderName, s.Options.Config.QueueProvider)
- opSC, err := s.StorageProvider.GetStorageClient(ctx, s.ProviderName+"/operationstatuses")
- if err != nil {
- return err
- }
+ qp := qprovider.New(s.Options.Config.QueueProvider)
+ var err error
s.RequestQueue, err = qp.GetClient(ctx)
if err != nil {
return err
}
- s.OperationStatusManager = manager.New(opSC, s.RequestQueue, s.ProviderName, s.Options.Config.Env.RoleLocation)
+ s.OperationStatusManager = manager.New(s.StorageProvider, s.RequestQueue, s.Options.Config.Env.RoleLocation)
s.Controllers = NewControllerRegistry(s.StorageProvider)
-
- if s.Options.K8sConfig != nil {
- s.KubeClient, err = kubeutil.NewRuntimeClient(s.Options.K8sConfig)
- if err != nil {
- return err
- }
-
- s.KubeClientSet, err = kubernetes.NewForConfig(s.Options.K8sConfig)
- if err != nil {
- return err
- }
-
- discoveryClient, err := discovery.NewDiscoveryClientForConfig(s.Options.K8sConfig)
- if err != nil {
- return err
- }
-
- // Use legacy discovery client to avoid the issue of the staled GroupVersion discovery(api.ucp.dev/v1alpha3).
- // TODO: Disable UseLegacyDiscovery once https://github.com/radius-project/radius/issues/5974 is resolved.
- discoveryClient.UseLegacyDiscovery = true
- s.KubeDiscoveryClient = discoveryClient
-
- s.KubeDynamicClientSet, err = dynamic.NewForConfig(s.Options.K8sConfig)
- if err != nil {
- return err
- }
- }
return nil
}
diff --git a/pkg/armrpc/builder/builder.go b/pkg/armrpc/builder/builder.go
new file mode 100644
index 0000000000..3284b7968a
--- /dev/null
+++ b/pkg/armrpc/builder/builder.go
@@ -0,0 +1,184 @@
+/*
+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 builder
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi/v5"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
+ apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/server"
+ "github.com/radius-project/radius/pkg/validator"
+ "github.com/radius-project/radius/swagger"
+)
+
+// Builder can be used to register operations and build HTTP routing paths and handlers for a resource namespace.
+type Builder struct {
+ namespaceNode *Namespace
+ registrations []*OperationRegistration
+}
+
+// defaultHandlerOptions returns HandlerOption for the default operations such as getting operationStatuses and
+// operationResults.
+func defaultHandlerOptions(
+ ctx context.Context,
+ rootRouter chi.Router,
+ rootScopePath string,
+ namespace string,
+ availableOperations []v1.Operation,
+ ctrlOpts apictrl.Options) []server.HandlerOptions {
+ namespace = strings.ToLower(namespace)
+
+ handlers := []server.HandlerOptions{}
+ if len(availableOperations) > 0 {
+ // https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/proxy-api-reference.md#exposing-available-operations
+ handlers = append(handlers, server.HandlerOptions{
+ ParentRouter: rootRouter,
+ Path: rootScopePath + "/providers/" + namespace + "/operations",
+ ResourceType: namespace + "/operations",
+ Method: v1.OperationGet,
+ ControllerFactory: func(op apictrl.Options) (apictrl.Controller, error) {
+ return defaultoperation.NewGetOperations(op, availableOperations)
+ },
+ })
+ }
+
+ statusType := namespace + "/operationstatuses"
+ resultType := namespace + "/operationresults"
+ handlers = append(handlers, server.HandlerOptions{
+ ParentRouter: rootRouter,
+ Path: fmt.Sprintf("%s/providers/%s/locations/{location}/operationstatuses/{operationId}", rootScopePath, namespace),
+ ResourceType: statusType,
+ Method: v1.OperationGet,
+ ControllerFactory: defaultoperation.NewGetOperationStatus,
+ })
+
+ handlers = append(handlers, server.HandlerOptions{
+ ParentRouter: rootRouter,
+ Path: fmt.Sprintf("%s/providers/%s/locations/{location}/operationresults/{operationId}", rootScopePath, namespace),
+ ResourceType: resultType,
+ Method: v1.OperationGet,
+ ControllerFactory: defaultoperation.NewGetOperationResult,
+ })
+
+ return handlers
+}
+
+func (b *Builder) Namespace() string {
+ return b.namespaceNode.Name
+}
+
+const (
+ UCPRootScopePath = "/planes/radius/{planeName}"
+ ResourceGroupPath = "/resourcegroups/{resourceGroupName}"
+)
+
+// NewOpenAPIValidatorMiddleware creates a new OpenAPI validator middleware.
+func NewOpenAPIValidator(ctx context.Context, base, namespace string) (func(h http.Handler) http.Handler, error) {
+ rootScopePath := base + UCPRootScopePath
+
+ // URLs may use either the subscription/plane scope or resource group scope.
+ // These paths are order sensitive and the longer path MUST be registered first.
+ prefixes := []string{
+ rootScopePath + ResourceGroupPath,
+ rootScopePath,
+ }
+
+ specLoader, err := validator.LoadSpec(ctx, namespace, swagger.SpecFiles, prefixes, "rootScope")
+ if err != nil {
+ return nil, err
+ }
+
+ return validator.APIValidator(validator.Options{
+ SpecLoader: specLoader,
+ ResourceTypeGetter: validator.RadiusResourceTypeGetter,
+ }), nil
+}
+
+// ApplyAPIHandlers builds HTTP routing paths and handlers for namespace.
+func (b *Builder) ApplyAPIHandlers(ctx context.Context, r chi.Router, ctrlOpts apictrl.Options, middlewares ...func(h http.Handler) http.Handler) error {
+ rootScopePath := ctrlOpts.PathBase + UCPRootScopePath
+
+ // Configure the default handlers.
+ handlerOptions := defaultHandlerOptions(ctx, r, rootScopePath, b.namespaceNode.Name, b.namespaceNode.availableOperations, ctrlOpts)
+
+ routerMap := map[string]chi.Router{}
+ for _, h := range b.registrations {
+ if h == nil {
+ continue
+ }
+
+ key := ""
+ route := ""
+ switch h.Method {
+ case v1.OperationPlaneScopeList:
+ route = fmt.Sprintf("%s/providers/%s", rootScopePath, strings.ToLower(h.ResourceType))
+ key = "plane-" + h.ResourceType
+ case v1.OperationList:
+ route = fmt.Sprintf("%s/resourcegroups/{resourceGroupName}/providers/%s", rootScopePath, h.ResourceNamePattern)
+ key = "rg-" + h.ResourceType
+ default:
+ route = fmt.Sprintf("%s/resourcegroups/{resourceGroupName}/providers/%s", rootScopePath, h.ResourceNamePattern)
+ key = "resource-" + h.ResourceNamePattern
+ }
+
+ if _, ok := routerMap[key]; !ok {
+ routerMap[key] = server.NewSubrouter(r, route, middlewares...)
+ }
+
+ handlerOptions = append(handlerOptions, server.HandlerOptions{
+ ParentRouter: routerMap[key],
+ Path: strings.ToLower(h.Path),
+ ResourceType: h.ResourceType,
+ Method: h.Method,
+ ControllerFactory: h.APIController,
+ })
+ }
+
+ for _, o := range handlerOptions {
+ if err := server.RegisterHandler(ctx, o, ctrlOpts); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// ApplyAsyncHandler registers asynchronous controllers from HandlerOutput.
+func (b *Builder) ApplyAsyncHandler(ctx context.Context, registry *worker.ControllerRegistry, ctrlOpts asyncctrl.Options) error {
+ for _, h := range b.registrations {
+ if h == nil {
+ continue
+ }
+
+ if h.AsyncController != nil {
+ err := registry.Register(ctx, h.ResourceType, h.Method, h.AsyncController, ctrlOpts)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/pkg/armrpc/builder/builder_test.go b/pkg/armrpc/builder/builder_test.go
new file mode 100644
index 0000000000..480aa69765
--- /dev/null
+++ b/pkg/armrpc/builder/builder_test.go
@@ -0,0 +1,267 @@
+/*
+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 builder
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/golang/mock/gomock"
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
+ apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/radius-project/radius/pkg/ucp/dataprovider"
+ "github.com/radius-project/radius/pkg/ucp/store"
+ "github.com/radius-project/radius/test/testcontext"
+ "github.com/stretchr/testify/require"
+)
+
+var handlerTests = []rpctest.HandlerTestSpec{
+ // applications.compute/virtualMachines
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.compute/virtualmachines",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTART"},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0/start",
+ Method: http.MethodPost,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTOP"},
+ Path: "/resourcegroups/testrg/providers/applications.compute/virtualmachines/vm0/stop",
+ Method: http.MethodPost,
+ },
+ // applications.compute/containers
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.compute/containers",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers", Method: "ACTIONGETRESOURCE"},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/getresource",
+ Method: http.MethodPost,
+ },
+ // applications.compute/containers/secrets
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/containers/secrets", Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.compute/containers/container0/secrets/secret0",
+ Method: http.MethodDelete,
+ },
+ // applications.compute/webassemblies
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.compute/webassemblies",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/webassemblies", Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.compute/webassemblies/wasm0",
+ Method: http.MethodDelete,
+ },
+}
+
+var defaultHandlerTests = []rpctest.HandlerTestSpec{
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/operations", Method: v1.OperationGet},
+ Path: "/providers/applications.compute/operations",
+ Method: http.MethodGet,
+ },
+ // default operations
+ {
+ OperationType: v1.OperationType{Type: "Applications.Compute/operationStatuses", Method: v1.OperationGet},
+ Path: "/providers/applications.compute/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Compute/operationResults", Method: v1.OperationGet},
+ Path: "/providers/applications.compute/locations/global/operationresults/00000000-0000-0000-0000-000000000000",
+ Method: http.MethodGet,
+ },
+}
+
+func setup(t *testing.T) (*dataprovider.MockDataStorageProvider, *store.MockStorageClient) {
+ mctrl := gomock.NewController(t)
+
+ mockSP := dataprovider.NewMockDataStorageProvider(mctrl)
+ mockSC := store.NewMockStorageClient(mctrl)
+
+ mockSC.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.Object{}, nil).AnyTimes()
+ mockSC.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+ mockSC.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+ mockSC.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.ObjectQueryResult{}, nil).AnyTimes()
+ mockSP.EXPECT().GetStorageClient(gomock.Any(), gomock.Any()).Return(store.StorageClient(mockSC), nil).AnyTimes()
+
+ return mockSP, mockSC
+}
+
+func TestApplyAPIHandlers(t *testing.T) {
+ mockSP, _ := setup(t)
+
+ runTests := func(t *testing.T, testSpecs []rpctest.HandlerTestSpec, b *Builder) {
+ rpctest.AssertRequests(t, testSpecs, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) {
+ r := chi.NewRouter()
+ return r, b.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP})
+ })
+ }
+
+ t.Run("custom handlers", func(t *testing.T) {
+ ns := newTestNamespace(t)
+ builder := ns.GenerateBuilder()
+ runTests(t, handlerTests, &builder)
+ })
+
+ t.Run("default handlers", func(t *testing.T) {
+ ns := newTestNamespace(t)
+ builder := ns.GenerateBuilder()
+ ns.SetAvailableOperations([]v1.Operation{
+ {
+ Name: "Applications.Compute/operations/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Compute",
+ Resource: "operations",
+ Operation: "Get operations",
+ Description: "Get the list of operations",
+ },
+ IsDataAction: false,
+ },
+ })
+ runTests(t, defaultHandlerTests, &builder)
+ })
+}
+
+func TestApplyAPIHandlers_AvailableOperations(t *testing.T) {
+ mockSP, _ := setup(t)
+ ns := newTestNamespace(t)
+
+ ns.SetAvailableOperations([]v1.Operation{
+ {
+ Name: "Applications.Compute/operations/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Compute",
+ Resource: "operations",
+ Operation: "Get operations",
+ Description: "Get the list of operations",
+ },
+ IsDataAction: false,
+ },
+ })
+
+ builder := ns.GenerateBuilder()
+ rpctest.AssertRequests(t, handlerTests, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) {
+ r := chi.NewRouter()
+ return r, builder.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP})
+ })
+}
+
+func TestApplyAsyncHandler(t *testing.T) {
+ mockSP, _ := setup(t)
+ ns := newTestNamespace(t)
+ builder := ns.GenerateBuilder()
+ registry := worker.NewControllerRegistry(mockSP)
+ ctx := testcontext.New(t)
+ err := builder.ApplyAsyncHandler(ctx, registry, asyncctrl.Options{})
+ require.NoError(t, err)
+
+ expectedOperations := []v1.OperationType{
+ {Type: "Applications.Compute/virtualMachines", Method: v1.OperationPut},
+ {Type: "Applications.Compute/virtualMachines", Method: v1.OperationPatch},
+ {Type: "Applications.Compute/virtualMachines", Method: "ACTIONSTART"},
+ {Type: "Applications.Compute/virtualMachines/disks", Method: v1.OperationPut},
+ {Type: "Applications.Compute/virtualMachines/disks", Method: v1.OperationPatch},
+ {Type: "Applications.Compute/webAssemblies", Method: v1.OperationPut},
+ {Type: "Applications.Compute/webAssemblies", Method: v1.OperationPatch},
+ }
+
+ for _, op := range expectedOperations {
+ jobCtrl := registry.Get(op)
+ require.NotNil(t, jobCtrl)
+ }
+}
diff --git a/pkg/armrpc/builder/namespace.go b/pkg/armrpc/builder/namespace.go
new file mode 100644
index 0000000000..e0791e04a0
--- /dev/null
+++ b/pkg/armrpc/builder/namespace.go
@@ -0,0 +1,84 @@
+/*
+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 builder
+
+import (
+ "strings"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+)
+
+// Namespace represents the namespace of UCP.
+type Namespace struct {
+ ResourceNode
+
+ // availableOperations is the list of available operations for the namespace.
+ availableOperations []v1.Operation
+}
+
+// NewNamespace creates a new namespace.
+func NewNamespace(namespace string) *Namespace {
+ return &Namespace{
+ ResourceNode: ResourceNode{
+ Kind: NamespaceResourceKind,
+ Name: namespace,
+ children: make(map[string]*ResourceNode),
+ },
+ }
+}
+
+// SetAvailableOperations sets the available operations for the namespace.
+func (p *Namespace) SetAvailableOperations(operations []v1.Operation) {
+ p.availableOperations = operations
+}
+
+// GenerateBuilder Builder object by traversing resource nodes from namespace.
+func (p *Namespace) GenerateBuilder() Builder {
+ return Builder{
+ namespaceNode: p,
+ registrations: p.resolve(&p.ResourceNode, p.Name, strings.ToLower(p.Name)),
+ }
+}
+
+func (p *Namespace) resolve(node *ResourceNode, qualifiedType string, qualifiedPattern string) []*OperationRegistration {
+ outputs := []*OperationRegistration{}
+
+ newType := qualifiedType
+ newPattern := qualifiedPattern
+
+ if node.Kind != NamespaceResourceKind {
+ newType = qualifiedType + "/" + node.Name
+ newPattern = qualifiedPattern + "/" + strings.ToLower(node.Name)
+ newParamName := "{" + node.option.ParamName() + "}"
+
+ // This builds the handler outputs for each resource type.
+ ctrls := node.option.BuildHandlerOutputs(BuildOptions{
+ ResourceType: newType,
+ ParameterName: newParamName,
+ ResourceNamePattern: newPattern,
+ })
+
+ newPattern += "/" + newParamName
+ outputs = append(outputs, ctrls...)
+ }
+
+ for _, child := range node.children {
+ outputs = append(outputs, p.resolve(child, newType, newPattern)...)
+ }
+
+ return outputs
+}
diff --git a/pkg/armrpc/builder/namespace_test.go b/pkg/armrpc/builder/namespace_test.go
new file mode 100644
index 0000000000..b247c72810
--- /dev/null
+++ b/pkg/armrpc/builder/namespace_test.go
@@ -0,0 +1,445 @@
+/*
+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 builder
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ apictrl "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/ucp/store"
+ "github.com/stretchr/testify/require"
+)
+
+type testAPIController struct {
+ apictrl.Operation[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]
+}
+
+func (e *testAPIController) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) {
+ return nil, nil
+}
+
+type testAsyncController struct {
+}
+
+func (c *testAsyncController) Run(ctx context.Context, request *asyncctrl.Request) (asyncctrl.Result, error) {
+ return asyncctrl.Result{}, nil
+}
+
+func (c *testAsyncController) StorageClient() store.StorageClient {
+ return nil
+}
+
+func newTestController(opts apictrl.Options) (apictrl.Controller, error) {
+ return &testAPIController{
+ apictrl.NewOperation(opts,
+ apictrl.ResourceOptions[rpctest.TestResourceDataModel]{
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+ },
+ ),
+ }, nil
+}
+
+func newTestNamespace(t *testing.T) *Namespace {
+ ns := NewNamespace("Applications.Compute")
+ require.Equal(t, NamespaceResourceKind, ns.Kind)
+ require.Equal(t, "Applications.Compute", ns.Name)
+
+ asyncFunc := func(opts asyncctrl.Options) (asyncctrl.Controller, error) {
+ return &testAsyncController{}, nil
+ }
+
+ // register virtualMachines resource
+ vmResource := ns.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ ResourceParamName: "virtualMachineName",
+
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "start": {
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ "stop": {
+ APIController: newTestController,
+ },
+ },
+ })
+
+ require.NotNil(t, vmResource)
+
+ // register virtualMachines/disks child resource
+ _ = vmResource.AddResource("disks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "replace": {
+ APIController: newTestController,
+ },
+ },
+ })
+
+ // register virtualMachines/networks child resource
+ _ = vmResource.AddResource("networks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "connect": {
+ APIController: newTestController,
+ },
+ },
+ })
+
+ // register containers resource
+ containerResource := ns.AddResource("containers", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "getresource": {
+ APIController: newTestController,
+ },
+ },
+ })
+
+ require.NotNil(t, containerResource)
+
+ // register containers/secrets child resource
+ _ = containerResource.AddResource("secrets", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ },
+ })
+
+ // register webAssemblies resource
+ wasmResource := ns.AddResource("webAssemblies", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ ResourceParamName: "webAssemblyName",
+ RequestConverter: rpctest.TestResourceDataModelFromVersioned,
+ ResponseConverter: rpctest.TestResourceDataModelToVersioned,
+
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: newTestController,
+ AsyncJobController: asyncFunc,
+ },
+ })
+
+ require.NotNil(t, wasmResource)
+
+ return ns
+}
+
+func TestNamespaceBuild(t *testing.T) {
+ ns := newTestNamespace(t)
+ builders := ns.GenerateBuilder()
+ require.NotNil(t, builders)
+
+ builderTests := []struct {
+ resourceType string
+ resourceNamePattern string
+ path string
+ method v1.OperationMethod
+ found bool
+ }{
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines",
+ path: "",
+ method: "LISTPLANESCOPE",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "",
+ method: "DELETE",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "/start",
+ method: "ACTIONSTART",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ path: "/stop",
+ method: "ACTIONSTOP",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}",
+ path: "",
+ method: "DELETE",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/networks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/networks/{networkName}",
+ path: "/connect",
+ method: "ACTIONCONNECT",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}",
+ path: "",
+ method: "DELETE",
+ },
+ {
+ resourceType: "Applications.Compute/virtualMachines/disks",
+ resourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}/disks/{diskName}",
+ path: "/replace",
+ method: "ACTIONREPLACE",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers",
+ path: "",
+ method: "LISTPLANESCOPE",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers/{containerName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers/{containerName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers/{containerName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers/{containerName}",
+ path: "",
+ method: "DELETE",
+ },
+ {
+ resourceType: "Applications.Compute/containers",
+ resourceNamePattern: "applications.compute/containers/{containerName}",
+ path: "/getresource",
+ method: "ACTIONGETRESOURCE",
+ },
+ {
+ resourceType: "Applications.Compute/containers/secrets",
+ resourceNamePattern: "applications.compute/containers/{containerName}/secrets",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/containers/secrets",
+ resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/containers/secrets",
+ resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/containers/secrets",
+ resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/containers/secrets",
+ resourceNamePattern: "applications.compute/containers/{containerName}/secrets/{secretName}",
+ path: "",
+ method: "DELETE",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies",
+ path: "",
+ method: "LISTPLANESCOPE",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies",
+ path: "",
+ method: "LIST",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}",
+ path: "",
+ method: "GET",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}",
+ path: "",
+ method: "PUT",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}",
+ path: "",
+ method: "PATCH",
+ },
+ {
+ resourceType: "Applications.Compute/webAssemblies",
+ resourceNamePattern: "applications.compute/webassemblies/{webAssemblyName}",
+ path: "",
+ method: "DELETE",
+ },
+ }
+
+ for _, b := range builders.registrations {
+ for i, bt := range builderTests {
+ if bt.resourceType == b.ResourceType && bt.resourceNamePattern == b.ResourceNamePattern && bt.path == b.Path && bt.method == b.Method {
+ builderTests[i].found = true
+ }
+ }
+ }
+
+ for _, bt := range builderTests {
+ require.True(t, bt.found, "resource not found: %s %s %s %s", bt.resourceType, bt.resourceNamePattern, bt.path, bt.method)
+ }
+}
diff --git a/pkg/armrpc/builder/node.go b/pkg/armrpc/builder/node.go
new file mode 100644
index 0000000000..06c54a493c
--- /dev/null
+++ b/pkg/armrpc/builder/node.go
@@ -0,0 +1,73 @@
+/*
+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 builder
+
+import (
+ "errors"
+ "strings"
+)
+
+var (
+ // ErrResourceAlreadyExists represents an error when a resource already exists.
+ ErrResourceAlreadyExists = errors.New("resource already exists")
+)
+
+// ResourceNode is a node in the resource tree.
+type ResourceNode struct {
+ // Kind is the resource kind.
+ Kind ResourceKind
+
+ // Name is the resource name.
+ Name string
+
+ // option includes the resource handlers.
+ option ResourceOptionBuilder
+
+ // children includes the child resources and custom actions of this resource.
+ children map[string]*ResourceNode
+}
+
+// AddResource adds a new child resource type and API handlers and returns new resource node.
+func (r *ResourceNode) AddResource(name string, option ResourceOptionBuilder) *ResourceNode {
+ normalized := strings.ToLower(name)
+
+ if _, ok := r.children[normalized]; ok {
+ panic(ErrResourceAlreadyExists)
+ }
+
+ child := &ResourceNode{
+ Name: name,
+ children: make(map[string]*ResourceNode),
+ option: option,
+ }
+
+ switch r.Kind {
+ case NamespaceResourceKind:
+ child.Kind = TrackedResourceKind
+
+ case TrackedResourceKind:
+ child.Kind = ProxyResourceKind
+
+ default:
+ child.Kind = ProxyResourceKind
+ }
+
+ option.LinkResource(child)
+ r.children[normalized] = child
+
+ return child
+}
diff --git a/pkg/armrpc/builder/node_test.go b/pkg/armrpc/builder/node_test.go
new file mode 100644
index 0000000000..865f404198
--- /dev/null
+++ b/pkg/armrpc/builder/node_test.go
@@ -0,0 +1,47 @@
+/*
+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 builder
+
+import (
+ "testing"
+
+ "github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAddResource(t *testing.T) {
+ r := &ResourceNode{
+ Name: "Applications.Core",
+ Kind: NamespaceResourceKind,
+ children: make(map[string]*ResourceNode),
+ }
+
+ child := r.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{})
+ require.Equal(t, "virtualMachines", child.Name)
+ require.Equal(t, TrackedResourceKind, child.Kind, "child resource of namespace should be a tracked resource")
+ require.Len(t, r.children, 1, "should have one child resource")
+
+ require.Panics(t, func() {
+ _ = r.AddResource("virtualMachines", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{})
+ }, "panic when adding a resource with the same name")
+
+ nested := child.AddResource("disks", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{})
+ _ = child.AddResource("cpus", &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{})
+ require.Equal(t, "disks", nested.Name)
+ require.Equal(t, ProxyResourceKind, nested.Kind, "nested resource should be a proxy resource")
+ require.Len(t, child.children, 2, "should have 2 child resource")
+}
diff --git a/pkg/armrpc/builder/operation.go b/pkg/armrpc/builder/operation.go
new file mode 100644
index 0000000000..b491604525
--- /dev/null
+++ b/pkg/armrpc/builder/operation.go
@@ -0,0 +1,367 @@
+/*
+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 builder
+
+import (
+ "strings"
+ "time"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/server"
+)
+
+const customActionPrefix = "ACTION"
+
+// Operation defines converters for request and response, update and delete filters,
+// asynchronous operation controller, and the options for API operation.
+type Operation[T any] struct {
+ // Disabled indicates that the operation is disabled. By default, all operations are enabled.
+ Disabled bool
+
+ // UpdateFilters is a slice of filters that execute prior to updating a resource.
+ UpdateFilters []controller.UpdateFilter[T]
+
+ // DeleteFilters is a slice of filters that execute prior to deleting a resource.
+ DeleteFilters []controller.DeleteFilter[T]
+
+ // APIController is the controller function for the API controller frontend.
+ APIController server.ControllerFactoryFunc
+
+ // AsyncJobController is the controller function for the async job worker.
+ AsyncJobController worker.ControllerFactoryFunc
+
+ // AsyncOperationTimeout is the default timeout duration of async operations for the operation.
+ AsyncOperationTimeout time.Duration
+
+ // AsyncOperationRetryAfter is the value of the Retry-After header that will be used for async operations.
+ // If this is 0 then the default value of v1.DefaultRetryAfter will be used. Consider setting this to a smaller
+ // value like 5 seconds if your operations will complete quickly.
+ AsyncOperationRetryAfter time.Duration
+}
+
+// ResourceOption is the option for ResourceNode. It defines model converters for request and response
+// and configures operation for each CRUDL and custom actions.
+type ResourceOption[P interface {
+ *T
+ v1.ResourceDataModel
+}, T any] struct {
+ // linkedNode references to ResourceNode linked to this option.
+ linkedNode *ResourceNode
+
+ // ResourceParamName is the parameter name of the resource. This is optional.
+ // If not set, the parameter name will be generated by adding "Name" suffix to the resource name.
+ ResourceParamName string
+
+ // RequestConverter is the request converter.
+ RequestConverter v1.ConvertToDataModel[T]
+
+ // ResponseConverter is the response converter.
+ ResponseConverter v1.ConvertToAPIModel[T]
+
+ // ListPlane defines the operation for listing resources by plane scope.
+ ListPlane Operation[T]
+
+ // List defines the operation for listing resources by resource group scope.
+ List Operation[T]
+
+ // Get defines the operation for getting a resource.
+ Get Operation[T]
+
+ // Put defines the operation for creating or updating a resource.
+ Put Operation[T]
+
+ // Patch defines the operation for updating a resource.
+ Patch Operation[T]
+
+ // Delete defines the operation for deleting a resource.
+ Delete Operation[T]
+
+ // Custom defines the custom actions.
+ Custom map[string]Operation[T]
+}
+
+// LinkResource links the resource node to the resource option.
+func (r *ResourceOption[P, T]) LinkResource(node *ResourceNode) {
+ r.linkedNode = node
+}
+
+// ParamName returns the parameter name of the resource.
+// If ResourceParamName is not set, the parameter name will be generated by adding "Name" suffix to the resource name.
+func (r *ResourceOption[P, T]) ParamName() string {
+ if r.ResourceParamName == "" {
+ typeName := r.linkedNode.Name
+ if strings.HasSuffix(typeName, "s") {
+ return typeName[:len(typeName)-1] + "Name"
+ } else {
+ return typeName + "Name"
+ }
+ }
+
+ return r.ResourceParamName
+}
+
+// BuildHandlerOutputs builds the handler outputs for each operation.
+func (r *ResourceOption[P, T]) BuildHandlerOutputs(opts BuildOptions) []*OperationRegistration {
+ handlerFuncs := []func(opts BuildOptions) *OperationRegistration{
+ r.listPlaneOutput,
+ r.listOutput,
+ r.getOutput,
+ r.putOutput,
+ r.patchOutput,
+ r.deleteOutput,
+ }
+
+ hs := []*OperationRegistration{}
+ for _, h := range handlerFuncs {
+ if out := h(opts); out != nil {
+ hs = append(hs, out)
+ }
+ }
+
+ return append(hs, r.customActionOutputs(opts)...)
+}
+
+func (r *ResourceOption[P, T]) listPlaneOutput(opts BuildOptions) *OperationRegistration {
+ if r.ListPlane.Disabled || r.linkedNode.Kind != TrackedResourceKind {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern,
+ Method: v1.OperationPlaneScopeList,
+ }
+
+ if r.ListPlane.APIController != nil {
+ h.APIController = r.ListPlane.APIController
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewListResources[P, T](opt,
+ controller.ResourceOptions[T]{
+ ResponseConverter: r.ResponseConverter,
+ ListRecursiveQuery: true,
+ },
+ )
+ }
+ }
+
+ return h
+}
+
+func (r *ResourceOption[P, T]) listOutput(opts BuildOptions) *OperationRegistration {
+ if r.List.Disabled {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern,
+ Method: v1.OperationList,
+ }
+
+ if r.List.APIController != nil {
+ h.APIController = r.List.APIController
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewListResources[P, T](opt,
+ controller.ResourceOptions[T]{
+ ResponseConverter: r.ResponseConverter,
+ },
+ )
+ }
+ }
+
+ return h
+}
+
+func (r *ResourceOption[P, T]) getOutput(opts BuildOptions) *OperationRegistration {
+ if r.Get.Disabled {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
+ Method: v1.OperationGet,
+ }
+
+ if r.Get.APIController != nil {
+ h.APIController = r.Get.APIController
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewGetResource[P, T](opt,
+ controller.ResourceOptions[T]{
+ ResponseConverter: r.ResponseConverter,
+ },
+ )
+ }
+ }
+
+ return h
+}
+
+func getOrDefaultAsyncOperationTimeout(d time.Duration) time.Duration {
+ if d == 0 {
+ return asyncctrl.DefaultAsyncOperationTimeout
+ }
+ return d
+}
+
+func getOrDefaultRetryAfter(d time.Duration) time.Duration {
+ if d == 0 {
+ return v1.DefaultRetryAfterDuration
+ }
+ return d
+}
+
+func (r *ResourceOption[P, T]) putOutput(opts BuildOptions) *OperationRegistration {
+ if r.Put.Disabled {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
+ Method: v1.OperationPut,
+ AsyncController: r.Delete.AsyncJobController,
+ }
+
+ if r.Put.APIController != nil {
+ h.APIController = r.Put.APIController
+ } else {
+ ro := controller.ResourceOptions[T]{
+ RequestConverter: r.RequestConverter,
+ ResponseConverter: r.ResponseConverter,
+ UpdateFilters: r.Put.UpdateFilters,
+ AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Put.AsyncOperationTimeout),
+ AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Put.AsyncOperationRetryAfter),
+ }
+
+ if r.Put.AsyncJobController == nil {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultSyncPut[P, T](opt, ro)
+ }
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro)
+ }
+ }
+ }
+ h.AsyncController = r.Put.AsyncJobController
+
+ return h
+}
+
+func (r *ResourceOption[P, T]) patchOutput(opts BuildOptions) *OperationRegistration {
+ if r.Patch.Disabled {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
+ Method: v1.OperationPatch,
+ AsyncController: r.Patch.AsyncJobController,
+ }
+
+ if r.Patch.APIController != nil {
+ h.APIController = r.Patch.APIController
+ } else {
+ ro := controller.ResourceOptions[T]{
+ RequestConverter: r.RequestConverter,
+ ResponseConverter: r.ResponseConverter,
+ UpdateFilters: r.Patch.UpdateFilters,
+ AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Patch.AsyncOperationTimeout),
+ AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Patch.AsyncOperationRetryAfter),
+ }
+
+ if r.Patch.AsyncJobController == nil {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultSyncPut[P, T](opt, ro)
+ }
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro)
+ }
+ }
+ }
+
+ return h
+}
+
+func (r *ResourceOption[P, T]) deleteOutput(opts BuildOptions) *OperationRegistration {
+ if r.Delete.Disabled {
+ return nil
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
+ Method: v1.OperationDelete,
+ AsyncController: r.Delete.AsyncJobController,
+ }
+
+ if r.Delete.APIController != nil {
+ h.APIController = r.Delete.APIController
+ } else {
+ ro := controller.ResourceOptions[T]{
+ RequestConverter: r.RequestConverter,
+ ResponseConverter: r.ResponseConverter,
+ DeleteFilters: r.Delete.DeleteFilters,
+ AsyncOperationTimeout: getOrDefaultAsyncOperationTimeout(r.Delete.AsyncOperationTimeout),
+ AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Delete.AsyncOperationRetryAfter),
+ }
+
+ if r.Delete.AsyncJobController == nil {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultSyncDelete[P, T](opt, ro)
+ }
+ } else {
+ h.APIController = func(opt controller.Options) (controller.Controller, error) {
+ return defaultoperation.NewDefaultAsyncDelete[P, T](opt, ro)
+ }
+ }
+ }
+
+ return h
+}
+
+func (r *ResourceOption[P, T]) customActionOutputs(opts BuildOptions) []*OperationRegistration {
+ handlers := []*OperationRegistration{}
+
+ for name, handle := range r.Custom {
+ if handle.APIController == nil {
+ panic("APIController is required for custom action")
+ }
+
+ h := &OperationRegistration{
+ ResourceType: opts.ResourceType,
+ ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
+ Path: "/" + strings.ToLower(name),
+ Method: v1.OperationMethod(customActionPrefix + strings.ToUpper(name)),
+ APIController: handle.APIController,
+ AsyncController: handle.AsyncJobController,
+ }
+ handlers = append(handlers, h)
+ }
+
+ return handlers
+}
diff --git a/pkg/armrpc/builder/operation_test.go b/pkg/armrpc/builder/operation_test.go
new file mode 100644
index 0000000000..4c8141fe19
--- /dev/null
+++ b/pkg/armrpc/builder/operation_test.go
@@ -0,0 +1,512 @@
+/*
+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 builder
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation"
+ "github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ testBuildOptions = BuildOptions{
+ ResourceType: "Applications.Compute/virtualMachines",
+ ResourceNamePattern: "applications.compute/virtualmachines",
+ }
+
+ testBuildOptionsWithName = BuildOptions{
+ ResourceType: "Applications.Compute/virtualMachines",
+ ResourceNamePattern: "applications.compute/virtualmachines",
+ ParameterName: "{virtualMachineName}",
+ }
+)
+
+func TestGetOrDefaultAsyncOperationTimeout(t *testing.T) {
+ var zeroDuration time.Duration
+ require.Equal(t, time.Duration(120)*time.Second, getOrDefaultAsyncOperationTimeout(zeroDuration))
+ require.Equal(t, time.Duration(1)*time.Minute, getOrDefaultAsyncOperationTimeout(time.Duration(1)*time.Minute))
+}
+
+func TestGetOrDefaultRetryAfter(t *testing.T) {
+ var zeroDuration time.Duration
+ require.Equal(t, time.Duration(60)*time.Second, getOrDefaultRetryAfter(zeroDuration))
+ require.Equal(t, time.Duration(1)*time.Minute, getOrDefaultRetryAfter(time.Duration(1)*time.Minute))
+}
+
+func TestResourceOption_LinkResource(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}
+ option.LinkResource(node)
+ require.Equal(t, node, option.linkedNode)
+}
+
+func TestResourceOption_ParamName(t *testing.T) {
+ t.Run("custom parameter name", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ ResourceParamName: "virtualMachineName",
+ }
+ require.Equal(t, "virtualMachineName", option.ParamName())
+ })
+
+ t.Run("plural resource type name", func(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}
+ option.LinkResource(node)
+ require.Equal(t, "virtualMachineName", option.ParamName())
+ })
+
+ t.Run("plural resource type name without s", func(t *testing.T) {
+ node := &ResourceNode{Name: "dice", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{}
+ option.LinkResource(node)
+ require.Equal(t, "diceName", option.ParamName())
+ })
+}
+
+func TestResourceOption_ListPlaneOutput(t *testing.T) {
+ t.Run("disabled is true", func(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ ListPlane: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.listPlaneOutput(BuildOptions{}))
+ })
+
+ t.Run("non tracked resource disabled operation", func(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: ProxyResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ ListPlane: Operation[rpctest.TestResourceDataModel]{},
+ }
+ require.Nil(t, option.listPlaneOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ ListPlane: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.listPlaneOutput(testBuildOptions)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationPlaneScopeList, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default controller", func(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ ListPlane: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.listPlaneOutput(testBuildOptions)
+ require.NotNil(t, h)
+ require.NotNil(t, h.APIController)
+ require.Equal(t, v1.OperationPlaneScopeList, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_ListOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ t.Run("disabled is true", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ List: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.listOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ List: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.listOutput(testBuildOptions)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationList, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ List: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.listOutput(testBuildOptions)
+ require.NotNil(t, h)
+ require.NotNil(t, h.APIController)
+ require.Equal(t, v1.OperationList, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_GetOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+
+ t.Run("disabled is true", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Get: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.getOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Get: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.getOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationGet, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Get: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.getOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.NotNil(t, h.APIController)
+ require.Equal(t, v1.OperationGet, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_PutOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+
+ t.Run("disabled is true", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Put: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.putOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Put: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.putOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationPut, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default sync controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Put: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.putOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationPut, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultSyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default async controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Put: Operation[rpctest.TestResourceDataModel]{
+ AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) {
+ return nil, nil
+ },
+ },
+ }
+ h := option.putOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationPut, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultAsyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_PatchOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+
+ t.Run("disabled is true", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.patchOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.patchOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationPatch, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default sync controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Patch: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.patchOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationPatch, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultSyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default async controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Patch: Operation[rpctest.TestResourceDataModel]{
+ AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) {
+ return nil, nil
+ },
+ },
+ }
+ h := option.patchOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationPatch, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultAsyncPut[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_DeleteOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+
+ t.Run("disabled is true", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Delete: Operation[rpctest.TestResourceDataModel]{
+ Disabled: true,
+ },
+ }
+ require.Nil(t, option.deleteOutput(BuildOptions{}))
+ })
+
+ t.Run("custom controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Delete: Operation[rpctest.TestResourceDataModel]{
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, errors.New("ok")
+ },
+ },
+ }
+ h := option.deleteOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ _, err := h.APIController(controller.Options{})
+ require.EqualError(t, err, "ok")
+ require.Equal(t, v1.OperationDelete, h.Method)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default sync controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Delete: Operation[rpctest.TestResourceDataModel]{},
+ }
+ h := option.deleteOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationDelete, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultSyncDelete[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+
+ t.Run("default async controller", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Delete: Operation[rpctest.TestResourceDataModel]{
+ AsyncJobController: func(opts asyncctrl.Options) (asyncctrl.Controller, error) {
+ return nil, nil
+ },
+ },
+ }
+ h := option.deleteOutput(testBuildOptionsWithName)
+ require.NotNil(t, h)
+ require.Equal(t, v1.OperationDelete, h.Method)
+
+ api, err := h.APIController(controller.Options{})
+ require.NoError(t, err)
+ _, ok := api.(*defaultoperation.DefaultAsyncDelete[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel])
+ require.True(t, ok)
+ require.Equal(t, "Applications.Compute/virtualMachines", h.ResourceType)
+ require.Equal(t, "applications.compute/virtualmachines/{virtualMachineName}", h.ResourceNamePattern)
+ require.Empty(t, h.Path)
+ })
+}
+
+func TestResourceOption_CustomActionOutput(t *testing.T) {
+ node := &ResourceNode{Name: "virtualMachines", Kind: TrackedResourceKind}
+ t.Run("valid custom action", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "start": {
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, nil
+ },
+ },
+ "stop": {
+ APIController: func(opt controller.Options) (controller.Controller, error) {
+ return nil, nil
+ },
+ },
+ },
+ }
+
+ hs := option.customActionOutputs(testBuildOptionsWithName)
+ require.Len(t, hs, 2)
+
+ require.NotNil(t, hs[0].APIController)
+ require.NotNil(t, hs[1].APIController)
+
+ // Reset APIController to nil for comparison
+ hs[0].APIController = nil
+ hs[1].APIController = nil
+
+ require.ElementsMatch(t, []*OperationRegistration{
+ {
+ ResourceType: "Applications.Compute/virtualMachines",
+ ResourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ Path: "/start",
+ Method: "ACTIONSTART",
+ },
+ {
+ ResourceType: "Applications.Compute/virtualMachines",
+ ResourceNamePattern: "applications.compute/virtualmachines/{virtualMachineName}",
+ Path: "/stop",
+ Method: "ACTIONSTOP",
+ },
+ }, hs)
+ })
+
+ t.Run("APIController is not defined", func(t *testing.T) {
+ option := &ResourceOption[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]{
+ linkedNode: node,
+ Custom: map[string]Operation[rpctest.TestResourceDataModel]{
+ "start": {},
+ },
+ }
+ require.Panics(t, func() {
+ _ = option.customActionOutputs(testBuildOptionsWithName)
+ })
+ })
+}
diff --git a/pkg/armrpc/builder/types.go b/pkg/armrpc/builder/types.go
new file mode 100644
index 0000000000..4b265c964d
--- /dev/null
+++ b/pkg/armrpc/builder/types.go
@@ -0,0 +1,82 @@
+/*
+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 builder
+
+import (
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/server"
+)
+
+// ResourceKind represents the kind of resource.
+type ResourceKind string
+
+const (
+ // NamespaceResourceKind represents the namespace resource kind.
+ NamespaceResourceKind ResourceKind = "Namespace"
+
+ // TrackedResourceKind represents the tracked resource kind.
+ TrackedResourceKind ResourceKind = "TrackedResource"
+
+ // ProxyResourceKind represents the proxy resource kind.
+ ProxyResourceKind ResourceKind = "ProxyResource"
+)
+
+// ResourceOptionBuilder is the interface for resource option.
+type ResourceOptionBuilder interface {
+ // LinkResource links the resource node to the resource option.
+ LinkResource(*ResourceNode)
+
+ // ParamName gets the resource name for resource type.
+ ParamName() string
+
+ // BuildHandlerOutputs builds the resource outputs which constructs the API routing path and handlers.
+ BuildHandlerOutputs(BuildOptions) []*OperationRegistration
+}
+
+// BuildOptions is the options for building resource outputs.
+type BuildOptions struct {
+ // ResourceType represents the resource type.
+ ResourceType string
+
+ // ParameterName represents the resource name for resource type.
+ ParameterName string
+
+ // ResourceNamePattern represents the resource name pattern used for HTTP routing path.
+ ResourceNamePattern string
+}
+
+// OperationRegistration is the output for building resource outputs.
+type OperationRegistration struct {
+ // ResourceType represents the resource type.
+ ResourceType string
+
+ // ResourceNamePattern represents the resource name pattern used for HTTP routing path.
+ ResourceNamePattern string
+
+ // Path represents additional custom action path after resource name.
+ Path string
+
+ // Method represents the operation method.
+ Method v1.OperationMethod
+
+ // APIController represents the API controller handler.
+ APIController server.ControllerFactoryFunc
+
+ // AsyncController represents the async controller handler.
+ AsyncController worker.ControllerFactoryFunc
+}
diff --git a/pkg/armrpc/frontend/controller/controller.go b/pkg/armrpc/frontend/controller/controller.go
index cea4130853..d7c8bab554 100644
--- a/pkg/armrpc/frontend/controller/controller.go
+++ b/pkg/armrpc/frontend/controller/controller.go
@@ -61,7 +61,7 @@ type Options struct {
// KubeClient is the Kubernetes controller runtime client.
KubeClient runtimeclient.Client
- // ResourceType is the string that represents the resource type. May be empty of the controller
+ // ResourceType is the string that represents the resource type. May be empty if the controller
// does not represent a single type of resource.
ResourceType string
diff --git a/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go b/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go
new file mode 100644
index 0000000000..6dcc1bab5b
--- /dev/null
+++ b/pkg/armrpc/frontend/defaultoperation/getavailableoperations.go
@@ -0,0 +1,50 @@
+/*
+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 defaultoperation
+
+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"
+)
+
+var _ ctrl.Controller = (*GetOperations)(nil)
+
+// GetOperations is the controller implementation to get arm rpc available operations.
+type GetOperations struct {
+ ctrl.BaseController
+
+ availableOperations []any
+}
+
+// NewGetOperations creates a new GetOperations controller and returns it, or returns an error if one occurs.
+func NewGetOperations(opts ctrl.Options, opsList []v1.Operation) (ctrl.Controller, error) {
+ ops := []any{}
+ for _, o := range opsList {
+ ops = append(ops, o)
+ }
+ return &GetOperations{ctrl.NewBaseController(opts), ops}, nil
+}
+
+// Run returns the list of available operations/permission for the resource provider at tenant level.
+// Spec: https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/proxy-api-reference.md#exposing-available-operations
+func (opctrl *GetOperations) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) {
+ return rest.NewOKResponse(&v1.PaginatedList{Value: opctrl.availableOperations}), nil
+}
diff --git a/pkg/armrpc/frontend/defaultoperation/getoperationresult.go b/pkg/armrpc/frontend/defaultoperation/getoperationresult.go
index bf2abbc946..e5f64effd8 100644
--- a/pkg/armrpc/frontend/defaultoperation/getoperationresult.go
+++ b/pkg/armrpc/frontend/defaultoperation/getoperationresult.go
@@ -56,12 +56,24 @@ func (e *GetOperationResult) Run(ctx context.Context, w http.ResponseWriter, req
return rest.NewBadRequestResponse(err.Error()), nil
}
- os := &manager.Status{}
- _, err = e.GetResource(ctx, id.String(), os)
- if err != nil && errors.Is(&store.ErrNotFound{ID: id.String()}, err) {
+ // Avoid using GetResource or e.StorageClient since they will use a different
+ // storage client than the one we want.
+ storageClient, err := e.DataProvider().GetStorageClient(ctx, id.ProviderNamespace()+"/operationstatuses")
+ if err != nil {
+ return nil, err
+ }
+
+ obj, err := storageClient.Get(ctx, id.String())
+ if errors.Is(&store.ErrNotFound{ID: id.String()}, err) {
return rest.NewNotFoundResponse(id), nil
}
+ os := &manager.Status{}
+ err = obj.As(os)
+ if err != nil {
+ return nil, err
+ }
+
if !os.Status.IsTerminal() {
headers := map[string]string{
"Location": req.URL.String(),
diff --git a/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go b/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go
index e9276d806c..978e9f42aa 100644
--- a/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go
+++ b/pkg/armrpc/frontend/defaultoperation/getoperationresult_test.go
@@ -28,7 +28,9 @@ import (
manager "github.com/radius-project/radius/pkg/armrpc/asyncoperation/statusmanager"
ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
"github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/radius-project/radius/pkg/ucp/dataprovider"
"github.com/radius-project/radius/pkg/ucp/store"
+ "github.com/radius-project/radius/test/testcontext"
"github.com/radius-project/radius/test/testutil"
"github.com/golang/mock/gomock"
@@ -36,27 +38,34 @@ import (
)
func TestGetOperationResultRun(t *testing.T) {
- mctrl := gomock.NewController(t)
- defer mctrl.Finish()
-
- mStorageClient := store.NewMockStorageClient(mctrl)
- ctx := context.Background()
-
rawDataModel := testutil.ReadFixture("operationstatus_datamodel.json")
osDataModel := &manager.Status{}
- _ = json.Unmarshal(rawDataModel, osDataModel)
+ err := json.Unmarshal(rawDataModel, osDataModel)
+ require.NoError(t, err)
rawExpectedOutput := testutil.ReadFixture("operationstatus_output.json")
expectedOutput := &v1.AsyncOperationStatus{}
- _ = json.Unmarshal(rawExpectedOutput, expectedOutput)
+ err = json.Unmarshal(rawExpectedOutput, expectedOutput)
+ require.NoError(t, err)
t.Run("get non-existing resource", func(t *testing.T) {
+ mctrl := gomock.NewController(t)
+
+ operationResultStoreClient := store.NewMockStorageClient(mctrl)
+ operationStatusStoreClient := store.NewMockStorageClient(mctrl)
+
+ dataProvider := dataprovider.NewMockDataStorageProvider(mctrl)
+ dataProvider.EXPECT().
+ GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses").
+ Return(operationStatusStoreClient, nil).
+ Times(1)
+
w := httptest.NewRecorder()
- req, err := rpctest.NewHTTPRequestFromJSON(ctx, http.MethodGet, operationStatusTestHeaderFile, nil)
+ req, err := rpctest.NewHTTPRequestFromJSON(testcontext.New(t), http.MethodGet, operationStatusTestHeaderFile, nil)
require.NoError(t, err)
ctx := rpctest.NewARMRequestContext(req)
- mStorageClient.
+ operationStatusStoreClient.
EXPECT().
Get(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) {
@@ -64,7 +73,8 @@ func TestGetOperationResultRun(t *testing.T) {
})
ctl, err := NewGetOperationResult(ctrl.Options{
- StorageClient: mStorageClient,
+ DataProvider: dataProvider,
+ StorageClient: operationResultStoreClient, // Will not be used.
})
require.NoError(t, err)
@@ -114,15 +124,25 @@ func TestGetOperationResultRun(t *testing.T) {
for _, tt := range opResTestCases {
t.Run(tt.desc, func(t *testing.T) {
+ mctrl := gomock.NewController(t)
+ operationResultStoreClient := store.NewMockStorageClient(mctrl)
+ operationStatusStoreClient := store.NewMockStorageClient(mctrl)
+
+ dataProvider := dataprovider.NewMockDataStorageProvider(mctrl)
+ dataProvider.EXPECT().
+ GetStorageClient(gomock.Any(), "Applications.Core/operationstatuses").
+ Return(operationStatusStoreClient, nil).
+ Times(1)
+
w := httptest.NewRecorder()
- req, err := rpctest.NewHTTPRequestFromJSON(ctx, http.MethodGet, operationStatusTestHeaderFile, nil)
+ req, err := rpctest.NewHTTPRequestFromJSON(testcontext.New(t), http.MethodGet, operationStatusTestHeaderFile, nil)
require.NoError(t, err)
ctx := rpctest.NewARMRequestContext(req)
osDataModel.Status = tt.provisioningState
osDataModel.RetryAfter = time.Second * 5
- mStorageClient.
+ operationStatusStoreClient.
EXPECT().
Get(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, id string, _ ...store.GetOptions) (*store.Object, error) {
@@ -133,7 +153,8 @@ func TestGetOperationResultRun(t *testing.T) {
})
ctl, err := NewGetOperationResult(ctrl.Options{
- StorageClient: mStorageClient,
+ DataProvider: dataProvider,
+ StorageClient: operationResultStoreClient, // Will not be used.
})
require.NoError(t, err)
diff --git a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json
index 95731f8d21..83bfbf97a2 100644
--- a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json
+++ b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_datamodel.json
@@ -5,8 +5,8 @@
"operationType": "PUT",
"location": "West US",
"status": "Succeeded",
- "startTime": "2022-05-16T10:24:58+0000",
- "endTime": "2022-05-16T17:24:58+0000",
+ "startTime": "2022-05-16T10:24:58.000000Z",
+ "endTime": "2022-05-16T17:24:58.000000Z",
"percentComplete": "100",
"properties": {
"provisioningState": "Succeeded"
diff --git a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json
index 65785327d9..42602e8d6f 100644
--- a/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json
+++ b/pkg/armrpc/frontend/defaultoperation/testdata/operationstatus_output.json
@@ -5,8 +5,8 @@
"operationType": "PUT",
"location": "West US",
"status": "Succeeded",
- "startTime": "2022-05-16T10:24:58+0000",
- "endTime": "2022-05-16T17:24:58+0000",
+ "startTime": "2022-05-16T10:24:58.000000Z",
+ "endTime": "2022-05-16T17:24:58.000000Z",
"percentComplete": "100",
"properties": {
"provisioningState": "Succeeded"
diff --git a/pkg/armrpc/frontend/server/handler.go b/pkg/armrpc/frontend/server/handler.go
index 612c150bd2..a32279f734 100644
--- a/pkg/armrpc/frontend/server/handler.go
+++ b/pkg/armrpc/frontend/server/handler.go
@@ -29,7 +29,6 @@ import (
ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
"github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation"
"github.com/radius-project/radius/pkg/armrpc/rest"
- "github.com/radius-project/radius/pkg/armrpc/servicecontext"
"github.com/radius-project/radius/pkg/ucp/ucplog"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"
@@ -47,7 +46,7 @@ var (
ErrInvalidOperationTypeOption = errors.New("the resource type and method must be specified if the operation type is not specified")
)
-type ControllerFunc func(ctrl.Options) (ctrl.Controller, error)
+type ControllerFactoryFunc func(ctrl.Options) (ctrl.Controller, error)
// HandlerOptions represents a controller to be registered with the server.
//
@@ -85,7 +84,7 @@ type HandlerOptions struct {
OperationType *v1.OperationType
// ControllerFactory is a function invoked to create the controller. Will be invoked once during server startup.
- ControllerFactory ControllerFunc
+ ControllerFactory ControllerFactoryFunc
// Middlewares are the middlewares to apply to the handler.
Middlewares []func(http.Handler) http.Handler
@@ -101,10 +100,16 @@ func NewSubrouter(parent chi.Router, path string, middlewares ...func(http.Handl
// HandlerForController creates a http.HandlerFunc function that runs resource provider frontend controller, renders a
// http response from the returned rest.Response, and handles the error as a default internal error if this controller returns error.
-func HandlerForController(controller ctrl.Controller) http.HandlerFunc {
+func HandlerForController(controller ctrl.Controller, operationType v1.OperationType) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
- addRequestAttributes(ctx, req)
+
+ rpcCtx := v1.ARMRequestContextFromContext(ctx)
+ // Set the operation type in the context.
+ rpcCtx.OperationType = operationType
+
+ // Add OTEL labels for the telemetry.
+ withOtelLabelsForRequest(req)
response, err := controller.Run(ctx, w, req)
if err != nil {
@@ -159,9 +164,8 @@ func RegisterHandler(ctx context.Context, opts HandlerOptions, ctrlOpts ctrl.Opt
return nil
}
- middlewares := append(opts.Middlewares, servicecontext.WithOperationType(*opts.OperationType))
- handler := HandlerForController(ctrl)
- namedRouter := opts.ParentRouter.With(middlewares...)
+ handler := HandlerForController(ctrl, *opts.OperationType)
+ namedRouter := opts.ParentRouter.With(opts.Middlewares...)
if opts.Path == CatchAllPath {
namedRouter.HandleFunc(opts.Path, handler)
} else {
@@ -171,13 +175,13 @@ func RegisterHandler(ctx context.Context, opts HandlerOptions, ctrlOpts ctrl.Opt
return nil
}
-func addRequestAttributes(ctx context.Context, req *http.Request) {
- labeler, ok := otelhttp.LabelerFromContext(ctx)
+func withOtelLabelsForRequest(req *http.Request) {
+ labeler, ok := otelhttp.LabelerFromContext(req.Context())
if !ok {
return
}
- armContext := v1.ARMRequestContextFromContext(ctx)
+ armContext := v1.ARMRequestContextFromContext(req.Context())
resourceID := armContext.ResourceID
if resourceID.IsResource() || resourceID.IsResourceCollection() {
@@ -193,7 +197,7 @@ func ConfigureDefaultHandlers(
rootScopePath string,
isAzureProvider bool,
providerNamespace string,
- operationCtrlFactory ControllerFunc,
+ operationCtrlFactory ControllerFactoryFunc,
ctrlOpts ctrl.Options) error {
providerNamespace = strings.ToLower(providerNamespace)
rt := providerNamespace + "/providers"
@@ -230,7 +234,7 @@ func ConfigureDefaultHandlers(
ParentRouter: rootRouter,
Path: opStatus,
ResourceType: statusRT,
- Method: v1.OperationGetOperationStatuses,
+ Method: v1.OperationGet,
ControllerFactory: defaultoperation.NewGetOperationStatus,
}, ctrlOpts)
if err != nil {
@@ -242,7 +246,7 @@ func ConfigureDefaultHandlers(
ParentRouter: rootRouter,
Path: opResult,
ResourceType: statusRT,
- Method: v1.OperationGetOperationResult,
+ Method: v1.OperationGet,
ControllerFactory: defaultoperation.NewGetOperationResult,
}, ctrlOpts)
if err != nil {
diff --git a/pkg/armrpc/frontend/server/handler_test.go b/pkg/armrpc/frontend/server/handler_test.go
index db7b005f24..d052c0be40 100644
--- a/pkg/armrpc/frontend/server/handler_test.go
+++ b/pkg/armrpc/frontend/server/handler_test.go
@@ -17,6 +17,7 @@ limitations under the License.
package server
import (
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -29,6 +30,8 @@ import (
"github.com/golang/mock/gomock"
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/middleware"
"github.com/radius-project/radius/pkg/ucp/dataprovider"
"github.com/radius-project/radius/test/testcontext"
@@ -261,3 +264,28 @@ func Test_HandlerErrInternal(t *testing.T) {
require.Equal(t, v1.CodeInternal, armerr.Error.Code)
require.Equal(t, armerr.Error.Message, "Internal error")
}
+
+type testAPIController struct {
+ ctrl.Operation[*rpctest.TestResourceDataModel, rpctest.TestResourceDataModel]
+}
+
+func (e *testAPIController) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) {
+ return nil, nil
+}
+
+func Test_HandlerForController_OperationType(t *testing.T) {
+ expectedType := v1.OperationType{Type: "Applications.Compute/virtualMachines", Method: "GET"}
+
+ handler := HandlerForController(&testAPIController{}, expectedType)
+ w := httptest.NewRecorder()
+
+ req, err := http.NewRequest(http.MethodGet, "", bytes.NewBuffer([]byte{}))
+ require.NoError(t, err)
+
+ rCtx := &v1.ARMRequestContext{}
+ req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx))
+
+ handler.ServeHTTP(w, req)
+
+ require.Equal(t, expectedType.String(), rCtx.OperationType.String())
+}
diff --git a/pkg/armrpc/frontend/server/server.go b/pkg/armrpc/frontend/server/server.go
index b69895580a..95e5550333 100644
--- a/pkg/armrpc/frontend/server/server.go
+++ b/pkg/armrpc/frontend/server/server.go
@@ -38,13 +38,13 @@ const (
)
type Options struct {
- ProviderNamespace string
- Location string
- Address string
- PathBase string
- EnableArmAuth bool
- Configure func(chi.Router) error
- ArmCertMgr *authentication.ArmCertManager
+ ServiceName string
+ Location string
+ Address string
+ PathBase string
+ EnableArmAuth bool
+ Configure func(chi.Router) error
+ ArmCertMgr *authentication.ArmCertManager
}
// New creates a frontend server that can listen on the provided address and serve requests - it creates an HTTP server with a router,
@@ -54,7 +54,7 @@ func New(ctx context.Context, options Options) (*http.Server, error) {
r := chi.NewRouter()
r.Use(middleware.Recoverer)
- r.Use(middleware.WithLogger(options.ProviderNamespace))
+ r.Use(middleware.WithLogger)
r.NotFound(validator.APINotFoundHandler())
r.MethodNotAllowed(validator.APIMethodNotAllowedHandler())
@@ -77,7 +77,7 @@ func New(ctx context.Context, options Options) (*http.Server, error) {
handlerFunc := otelhttp.NewHandler(
middleware.LowercaseURLPath(r),
- options.ProviderNamespace,
+ options.ServiceName,
otelhttp.WithMeterProvider(otel.GetMeterProvider()),
otelhttp.WithTracerProvider(otel.GetTracerProvider()))
diff --git a/pkg/armrpc/frontend/server/service.go b/pkg/armrpc/frontend/server/service.go
index 9a51ac2dc3..f46244cfbe 100644
--- a/pkg/armrpc/frontend/server/service.go
+++ b/pkg/armrpc/frontend/server/service.go
@@ -58,17 +58,12 @@ func (s *Service) Init(ctx context.Context) error {
logger := ucplog.FromContextOrDiscard(ctx)
s.StorageProvider = dataprovider.NewStorageProvider(s.Options.Config.StorageProvider)
- qp := qprovider.New(s.ProviderName, s.Options.Config.QueueProvider)
- opSC, err := s.StorageProvider.GetStorageClient(ctx, s.ProviderName+"/operationstatuses")
- if err != nil {
- return err
- }
+ qp := qprovider.New(s.Options.Config.QueueProvider)
reqQueueClient, err := qp.GetClient(ctx)
if err != nil {
return err
}
- s.OperationStatusManager = manager.New(opSC, reqQueueClient, s.ProviderName, s.Options.Config.Env.RoleLocation)
-
+ s.OperationStatusManager = manager.New(s.StorageProvider, reqQueueClient, s.Options.Config.Env.RoleLocation)
s.KubeClient, err = kubeutil.NewRuntimeClient(s.Options.K8sConfig)
if err != nil {
return err
diff --git a/pkg/armrpc/rpctest/routers.go b/pkg/armrpc/rpctest/routers.go
index 73300eb1f1..4838eed556 100644
--- a/pkg/armrpc/rpctest/routers.go
+++ b/pkg/armrpc/rpctest/routers.go
@@ -52,9 +52,9 @@ func AssertRouters(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope st
r, err := configureRouter(ctx)
require.NoError(t, err)
- t.Log("Avaiable routes:")
+ t.Log("Available routes:")
err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
- t.Logf("Method: %s, Path: %s", method, route)
+ t.Logf("Method: %s, Path: %s, Middlewares: %+v", method, route, middlewares)
return nil
})
require.NoError(t, err)
@@ -86,35 +86,48 @@ func AssertRouters(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope st
if tt.SkipOperationTypeValidation {
return
}
+ })
+ }
+}
+
+// AssertRequests asserts that the restful APIs matches the routes and its operation type matches the given test cases.
+// This is working only for test controllers. If you want to validate the routes for the real controllers, use AssertRouters.
+func AssertRequests(t *testing.T, tests []HandlerTestSpec, pathBase, rootScope string, configureRouter func(context.Context) (chi.Router, error)) {
+ ctx := testcontext.New(t)
+ r, err := configureRouter(ctx)
+ require.NoError(t, err)
+
+ t.Log("Available routes:")
+ err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
+ t.Logf("Method: %s, Path: %s, Middlewares: %+v", method, route, middlewares)
+ return nil
+ })
+ require.NoError(t, err)
+
+ for _, tt := range tests {
+ pb := ""
+ if !tt.SkipPathBase {
+ pb = pathBase
+ }
- err = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
- if tctx.RoutePattern() == route && tt.Method == method {
- found := false
- for _, m := range middlewares {
- w := httptest.NewRecorder()
-
- // It will not validate body.
- req, err := http.NewRequest(tt.Method, uri, bytes.NewBuffer([]byte{}))
- require.NoError(t, err)
-
- rCtx := &v1.ARMRequestContext{}
- req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx))
-
- // Pass empty router to validate operation type.
- testr := chi.NewRouter()
- m(testr).ServeHTTP(w, req)
- if tt.OperationType.String() == rCtx.OperationType.String() {
- t.Log("Found operation type")
- found = true
- break
- }
- }
- require.True(t, found, "operation type not found")
- }
- return nil
- })
+ uri := pb + rootScope + tt.Path
+ if tt.WithoutRootScope {
+ uri = pb + tt.Path
+ }
+
+ t.Run(tt.Method+"|"+tt.Path, func(t *testing.T) {
+ w := httptest.NewRecorder()
+ // It will not validate body.
+ req, err := http.NewRequest(tt.Method, uri, bytes.NewBuffer([]byte{}))
require.NoError(t, err)
+
+ rCtx := &v1.ARMRequestContext{}
+ req = req.WithContext(v1.WithARMRequestContext(context.Background(), rCtx))
+
+ r.ServeHTTP(w, req)
+ require.NotEqual(t, 404, w.Result().StatusCode)
+ require.Equal(t, tt.OperationType.String(), rCtx.OperationType.String(), "operation type not found: %s %s %s", uri, tt.Method, rCtx.OperationType.String())
})
}
}
diff --git a/pkg/armrpc/rpctest/testdatamodel.go b/pkg/armrpc/rpctest/testdatamodel.go
new file mode 100644
index 0000000000..bc6183bec0
--- /dev/null
+++ b/pkg/armrpc/rpctest/testdatamodel.go
@@ -0,0 +1,166 @@
+/*
+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 rpctest
+
+import (
+ "encoding/json"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ "github.com/radius-project/radius/pkg/to"
+)
+
+const (
+ TestAPIVersion = "2022-03-15-privatepreview"
+)
+
+// TestResourceDataModel represents test resource.
+type TestResourceDataModel struct {
+ v1.BaseResource
+
+ // Properties is the properties of the resource.
+ Properties *TestResourceDataModelProperties `json:"properties"`
+}
+
+// ResourceTypeName returns the qualified name of the resource
+func (r *TestResourceDataModel) ResourceTypeName() string {
+ return "Applications.Core/resources"
+}
+
+// TestResourceDataModelProperties represents the properties of TestResourceDataModel.
+type TestResourceDataModelProperties struct {
+ Application string `json:"application"`
+ Environment string `json:"environment"`
+ PropertyA string `json:"propertyA,omitempty"`
+ PropertyB string `json:"propertyB,omitempty"`
+}
+
+// TestResource represents test resource for api version.
+type TestResource struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ SystemData *v1.SystemData `json:"systemData,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Location *string `json:"location,omitempty"`
+ Properties *TestResourceProperties `json:"properties,omitempty"`
+ Tags map[string]*string `json:"tags,omitempty"`
+}
+
+// TestResourceProperties - HTTP Route properties
+type TestResourceProperties struct {
+ ProvisioningState *v1.ProvisioningState `json:"provisioningState,omitempty"`
+ Environment *string `json:"environment,omitempty"`
+ Application *string `json:"application,omitempty"`
+ PropertyA *string `json:"propertyA,omitempty"`
+ PropertyB *string `json:"propertyB,omitempty"`
+}
+
+// # Function Explanation
+//
+// ConvertTo converts a version specific TestResource into a version-agnostic resource, TestResourceDataModel.
+func (src *TestResource) ConvertTo() (v1.DataModelInterface, error) {
+ converted := &TestResourceDataModel{
+ BaseResource: v1.BaseResource{
+ TrackedResource: v1.TrackedResource{
+ ID: to.String(src.ID),
+ Name: to.String(src.Name),
+ Type: to.String(src.Type),
+ Location: to.String(src.Location),
+ Tags: to.StringMap(src.Tags),
+ },
+ InternalMetadata: v1.InternalMetadata{
+ UpdatedAPIVersion: TestAPIVersion,
+ AsyncProvisioningState: toProvisioningStateDataModel(src.Properties.ProvisioningState),
+ },
+ },
+ Properties: &TestResourceDataModelProperties{
+ Application: to.String(src.Properties.Application),
+ Environment: to.String(src.Properties.Environment),
+ PropertyA: to.String(src.Properties.PropertyA),
+ PropertyB: to.String(src.Properties.PropertyB),
+ },
+ }
+ return converted, nil
+}
+
+// # Function Explanation
+//
+// ConvertFrom converts src version agnostic model to versioned model, TestResource.
+func (dst *TestResource) ConvertFrom(src v1.DataModelInterface) error {
+ dm, ok := src.(*TestResourceDataModel)
+ if !ok {
+ return v1.ErrInvalidModelConversion
+ }
+
+ dst.ID = to.Ptr(dm.ID)
+ dst.Name = to.Ptr(dm.Name)
+ dst.Type = to.Ptr(dm.Type)
+ dst.SystemData = &dm.SystemData
+ dst.Location = to.Ptr(dm.Location)
+ dst.Tags = *to.StringMapPtr(dm.Tags)
+ dst.Properties = &TestResourceProperties{
+ ProvisioningState: fromProvisioningStateDataModel(dm.InternalMetadata.AsyncProvisioningState),
+ Environment: to.Ptr(dm.Properties.Environment),
+ Application: to.Ptr(dm.Properties.Application),
+ PropertyA: to.Ptr(dm.Properties.PropertyA),
+ PropertyB: to.Ptr(dm.Properties.PropertyB),
+ }
+
+ return nil
+}
+
+func toProvisioningStateDataModel(state *v1.ProvisioningState) v1.ProvisioningState {
+ if state == nil {
+ return v1.ProvisioningStateAccepted
+ }
+ return *state
+}
+
+func fromProvisioningStateDataModel(state v1.ProvisioningState) *v1.ProvisioningState {
+ converted := v1.ProvisioningStateAccepted
+ if state != "" {
+ converted = state
+ }
+
+ return &converted
+}
+
+func TestResourceDataModelToVersioned(model *TestResourceDataModel, version string) (v1.VersionedModelInterface, error) {
+ switch version {
+ case TestAPIVersion:
+ versioned := &TestResource{}
+ err := versioned.ConvertFrom(model)
+ return versioned, err
+
+ default:
+ return nil, v1.ErrUnsupportedAPIVersion
+ }
+}
+
+func TestResourceDataModelFromVersioned(content []byte, version string) (*TestResourceDataModel, error) {
+ switch version {
+ case TestAPIVersion:
+ am := &TestResource{}
+ if err := json.Unmarshal(content, am); err != nil {
+ return nil, err
+ }
+ dm, err := am.ConvertTo()
+ return dm.(*TestResourceDataModel), err
+
+ default:
+ return nil, v1.ErrUnsupportedAPIVersion
+ }
+}
diff --git a/pkg/corerp/backend/service.go b/pkg/corerp/backend/service.go
index e072b609b2..6b5dbc5986 100644
--- a/pkg/corerp/backend/service.go
+++ b/pkg/corerp/backend/service.go
@@ -30,6 +30,7 @@ import (
"github.com/radius-project/radius/pkg/corerp/datamodel"
"github.com/radius-project/radius/pkg/corerp/model"
"github.com/radius-project/radius/pkg/corerp/processors/extenders"
+ "github.com/radius-project/radius/pkg/kubeutil"
"github.com/radius-project/radius/pkg/portableresources"
pr_backend_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller"
"github.com/radius-project/radius/pkg/portableresources/processors"
@@ -84,16 +85,21 @@ func (w *Service) Run(ctx context.Context) error {
return err
}
- coreAppModel, err := model.NewApplicationModel(w.Options.Arm, w.KubeClient, w.KubeClientSet, w.KubeDiscoveryClient, w.KubeDynamicClientSet)
+ k8s, err := kubeutil.NewClients(w.Options.K8sConfig)
+ if err != nil {
+ return fmt.Errorf("failed to initialize kubernetes clients: %w", err)
+ }
+
+ coreAppModel, err := model.NewApplicationModel(w.Options.Arm, k8s.RuntimeClient, k8s.ClientSet, k8s.DiscoveryClient, k8s.DynamicClient)
if err != nil {
return fmt.Errorf("failed to initialize application model: %w", err)
}
opts := ctrl.Options{
DataProvider: w.StorageProvider,
- KubeClient: w.KubeClient,
+ KubeClient: k8s.RuntimeClient,
GetDeploymentProcessor: func() deployment.DeploymentProcessor {
- return deployment.NewDeploymentProcessor(coreAppModel, w.StorageProvider, w.KubeClient, w.KubeClientSet)
+ return deployment.NewDeploymentProcessor(coreAppModel, w.StorageProvider, k8s.RuntimeClient, k8s.ClientSet)
},
}
@@ -113,7 +119,7 @@ func (w *Service) Run(ctx context.Context) error {
}
}
- client := processors.NewResourceClient(w.Options.Arm, w.Options.UCPConnection, w.KubeClient, w.KubeDiscoveryClient)
+ client := processors.NewResourceClient(w.Options.Arm, w.Options.UCPConnection, k8s.RuntimeClient, k8s.DiscoveryClient)
clientOptions := sdk.NewClientOptions(w.Options.UCPConnection)
deploymentEngineClient, err := clients.NewResourceDeploymentsClient(&clients.Options{
@@ -133,18 +139,18 @@ func (w *Service) Run(ctx context.Context) error {
recipes.TemplateKindTerraform: driver.NewTerraformDriver(w.Options.UCPConnection, provider.NewSecretProvider(w.Options.Config.SecretProvider),
driver.TerraformOptions{
Path: w.Options.Config.Terraform.Path,
- }, w.KubeClientSet),
+ }, k8s.ClientSet),
},
})
opts.GetDeploymentProcessor = nil
extenderCreateOrUpdateController := func(options ctrl.Options) (ctrl.Controller, error) {
processor := &extenders.Processor{}
- return pr_backend_ctrl.NewCreateOrUpdateResource[*datamodel.Extender, datamodel.Extender](processor, engine, client, configLoader, options)
+ return pr_backend_ctrl.NewCreateOrUpdateResource[*datamodel.Extender, datamodel.Extender](options, processor, engine, client, configLoader)
}
extenderDeleteController := func(options ctrl.Options) (ctrl.Controller, error) {
processor := &extenders.Processor{}
- return pr_backend_ctrl.NewDeleteResource[*datamodel.Extender, datamodel.Extender](processor, engine, configLoader, options)
+ return pr_backend_ctrl.NewDeleteResource[*datamodel.Extender, datamodel.Extender](options, processor, engine, configLoader)
}
// Register controllers to run backend processing for extenders.
diff --git a/pkg/corerp/frontend/handler/getoperations_test.go b/pkg/corerp/frontend/handler/getoperations_test.go
deleted file mode 100644
index d43066d618..0000000000
--- a/pkg/corerp/frontend/handler/getoperations_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-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 handler
-
-import (
- "context"
- "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"
- v20220315privatepreview "github.com/radius-project/radius/pkg/corerp/api/v20220315privatepreview"
- "github.com/stretchr/testify/require"
-)
-
-func TestRunWith20220315PrivatePreview(t *testing.T) {
- // arrange
- opts := ctrl.Options{}
- op, err := NewGetOperations(opts)
- require.NoError(t, err)
- ctx := v1.WithARMRequestContext(context.Background(), &v1.ARMRequestContext{
- APIVersion: v20220315privatepreview.Version,
- })
- w := httptest.NewRecorder()
-
- // act
- resp, err := op.Run(ctx, w, nil)
-
- // assert
- require.NoError(t, err)
- switch v := resp.(type) {
- case *rest.OKResponse:
- pagination, ok := v.Body.(*v1.PaginatedList)
- require.True(t, ok)
- require.Equal(t, 24, len(pagination.Value))
- default:
- require.Truef(t, false, "should not return error")
- }
-}
-
-func TestRunWithUnsupportedAPIVersion(t *testing.T) {
- // arrange
- opts := ctrl.Options{}
- op, err := NewGetOperations(opts)
- require.NoError(t, err)
- ctx := v1.WithARMRequestContext(context.Background(), &v1.ARMRequestContext{
- APIVersion: "unknownversion",
- })
- w := httptest.NewRecorder()
-
- // act
- resp, err := op.Run(ctx, w, nil)
-
- // assert
- require.NoError(t, err)
- switch v := resp.(type) {
- case *rest.NotFoundResponse:
- armerr := v.Body
- require.Equal(t, v1.CodeInvalidResourceType, armerr.Error.Code)
- default:
- require.Truef(t, false, "should not return error")
- }
-}
diff --git a/pkg/corerp/frontend/handler/routes_test.go b/pkg/corerp/frontend/handler/routes_test.go
index 22ab8b1392..735b57091d 100644
--- a/pkg/corerp/frontend/handler/routes_test.go
+++ b/pkg/corerp/frontend/handler/routes_test.go
@@ -217,11 +217,11 @@ var handlerTests = []rpctest.HandlerTestSpec{
Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0",
Method: http.MethodDelete,
}, {
- OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGetOperationStatuses},
+ OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet},
Path: "/providers/applications.core/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
}, {
- OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGetOperationResult},
+ OperationType: v1.OperationType{Type: "Applications.Core/operationResults", Method: v1.OperationGet},
Path: "/providers/applications.core/locations/global/operationresults/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
},
diff --git a/pkg/corerp/frontend/service.go b/pkg/corerp/frontend/service.go
deleted file mode 100644
index 07125565ab..0000000000
--- a/pkg/corerp/frontend/service.go
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
-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 frontend
-
-import (
- "context"
- "fmt"
-
- "github.com/go-chi/chi/v5"
- ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
- "github.com/radius-project/radius/pkg/armrpc/frontend/server"
- "github.com/radius-project/radius/pkg/armrpc/hostoptions"
- "github.com/radius-project/radius/pkg/corerp/frontend/handler"
- "github.com/radius-project/radius/pkg/recipes"
- "github.com/radius-project/radius/pkg/recipes/driver"
- "github.com/radius-project/radius/pkg/recipes/engine"
- "github.com/radius-project/radius/pkg/sdk"
- "github.com/radius-project/radius/pkg/ucp/secret/provider"
-)
-
-type Service struct {
- server.Service
-}
-
-// NewService creates a new Service instance with the given options.
-func NewService(options hostoptions.HostOptions) *Service {
- return &Service{
- server.Service{
- Options: options,
- ProviderName: handler.ProviderNamespaceName,
- },
- }
-}
-
-// Name returns the namespace of the resource provider.
-func (s *Service) Name() string {
- return handler.ProviderNamespaceName
-}
-
-// Run initializes the service and starts the server with the specified options.
-func (s *Service) Run(ctx context.Context) error {
- if err := s.Init(ctx); err != nil {
- return err
- }
-
- // Creates a new engine with the drivers. The engine will be used to fetch Recipe parameter information from the template path.
- clientOptions := sdk.NewClientOptions(s.Options.UCPConnection)
- engine := engine.NewEngine(engine.Options{
- Drivers: map[string]driver.Driver{
- recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, nil, nil),
- recipes.TemplateKindTerraform: driver.NewTerraformDriver(s.Options.UCPConnection, provider.NewSecretProvider(s.Options.Config.SecretProvider),
- driver.TerraformOptions{
- Path: s.Options.Config.Terraform.Path,
- }, nil),
- },
- })
-
- opts := ctrl.Options{
- Address: fmt.Sprintf("%s:%d", s.Options.Config.Server.Host, s.Options.Config.Server.Port),
- PathBase: s.Options.Config.Server.PathBase,
- DataProvider: s.StorageProvider,
- KubeClient: s.KubeClient,
- StatusManager: s.OperationStatusManager,
- }
-
- err := s.Start(ctx, server.Options{
- Address: opts.Address,
- ProviderNamespace: s.ProviderName,
- Location: s.Options.Config.Env.RoleLocation,
- PathBase: s.Options.Config.Server.PathBase,
- // set the arm cert manager for managing client certificate
- ArmCertMgr: s.ARMCertManager,
- EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done
- Configure: func(router chi.Router) error {
- err := handler.AddRoutes(ctx, router, !hostoptions.IsSelfHosted(), opts, engine)
- if err != nil {
- return err
- }
-
- return nil
- }},
- )
- return err
-}
diff --git a/pkg/corerp/setup/operations.go b/pkg/corerp/setup/operations.go
new file mode 100644
index 0000000000..d132ae4800
--- /dev/null
+++ b/pkg/corerp/setup/operations.go
@@ -0,0 +1,262 @@
+/*
+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 setup
+
+import v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+
+var operationList = []v1.Operation{
+ {
+ Name: "Applications.Core/operations/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "operations",
+ Operation: "Get operations",
+ Description: "Get the list of operations",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/environments/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "environments",
+ Operation: "List environments",
+ Description: "Get the list of environments.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/environments/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "environments",
+ Operation: "Create/Update environment",
+ Description: "Create or update an environment.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/environments/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "environments",
+ Operation: "Delete environment",
+ Description: "Delete an environment.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/environments/getmetadata/action",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "environments",
+ Operation: "Get recipe metadata",
+ Description: "Get recipe metadata.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/environments/join/action",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "environments",
+ Operation: "Join environment",
+ Description: "Join to application environment.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/register/action",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "Applications.Core",
+ Operation: "Register Applications.Core",
+ Description: "Register the subscription for Applications.Core.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/unregister/action",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "Applications.Core",
+ Operation: "Unregister Applications.Core",
+ Description: "Unregister the subscription for Applications.Core.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/httproutes/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "httproutes",
+ Operation: "List httproutes",
+ Description: "Get the list of httproutes.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/httproutes/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "httproutes",
+ Operation: "Create/Update httproute",
+ Description: "Create or update an httproute.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/httproutes/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "httproutes",
+ Operation: "Delete httproute",
+ Description: "Delete an httproute.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/applications/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "applications",
+ Operation: "List applications",
+ Description: "Get the list of applications.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/applications/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "applications",
+ Operation: "Create/Update application",
+ Description: "Create or update an application.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/applications/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "applications",
+ Operation: "Delete application",
+ Description: "Delete an application.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/gateways/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "gateways",
+ Operation: "List gateways",
+ Description: "Get the list of gateways.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/gateways/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "gateways",
+ Operation: "Create/Update gateway",
+ Description: "Create or Update a gateway.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/gateways/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "gateways",
+ Operation: "delete gateway",
+ Description: "Delete a gateway.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/containers/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "containers",
+ Operation: "List containers",
+ Description: "Get the list of containers.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/containers/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "containers",
+ Operation: "Create/Update container",
+ Description: "Create or update a container.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/containers/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "containers",
+ Operation: "Delete container",
+ Description: "Delete a container.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/extenders/read",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "extenders",
+ Operation: "Get/List extenders",
+ Description: "Gets/Lists extender link(s).",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/extenders/write",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "extenders",
+ Operation: "Create/Update extenders",
+ Description: "Creates or updates a extender resource.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/extenders/delete",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "extenders",
+ Operation: "Delete extender",
+ Description: "Deletes a extender resource.",
+ },
+ IsDataAction: false,
+ },
+ {
+ Name: "Applications.Core/extenders/listsecrets/action",
+ Display: &v1.OperationDisplayProperties{
+ Provider: "Applications.Core",
+ Resource: "extenders",
+ Operation: "List secrets",
+ Description: "Lists extender secrets.",
+ },
+ IsDataAction: false,
+ },
+}
diff --git a/pkg/corerp/setup/setup.go b/pkg/corerp/setup/setup.go
new file mode 100644
index 0000000000..b4884fa5ad
--- /dev/null
+++ b/pkg/corerp/setup/setup.go
@@ -0,0 +1,217 @@
+/*
+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 setup
+
+import (
+ asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/builder"
+ apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/corerp/datamodel"
+ "github.com/radius-project/radius/pkg/corerp/datamodel/converter"
+ "github.com/radius-project/radius/pkg/recipes/controllerconfig"
+
+ backend_ctrl "github.com/radius-project/radius/pkg/corerp/backend/controller"
+ app_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/applications"
+ ctr_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/containers"
+ env_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/environments"
+ ext_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/extenders"
+ gw_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/gateways"
+ secret_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/secretstores"
+ vol_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/volumes"
+ rp_frontend "github.com/radius-project/radius/pkg/rp/frontend"
+
+ ext_processor "github.com/radius-project/radius/pkg/corerp/processors/extenders"
+ pr_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller"
+)
+
+// SetupNamespace builds the namespace for core resource provider.
+func SetupNamespace(recipeControllerConfig *controllerconfig.RecipeControllerConfig) *builder.Namespace {
+ ns := builder.NewNamespace("Applications.Core")
+
+ _ = ns.AddResource("environments", &builder.ResourceOption[*datamodel.Environment, datamodel.Environment]{
+ RequestConverter: converter.EnvironmentDataModelFromVersioned,
+ ResponseConverter: converter.EnvironmentDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.Environment]{
+ APIController: env_ctrl.NewCreateOrUpdateEnvironment,
+ },
+ Patch: builder.Operation[datamodel.Environment]{
+ APIController: env_ctrl.NewCreateOrUpdateEnvironment,
+ },
+ Custom: map[string]builder.Operation[datamodel.Environment]{
+ "getmetadata": {
+ APIController: func(opt apictrl.Options) (apictrl.Controller, error) {
+ return env_ctrl.NewGetRecipeMetadata(opt, recipeControllerConfig.Engine)
+ },
+ },
+ },
+ })
+
+ _ = ns.AddResource("applications", &builder.ResourceOption[*datamodel.Application, datamodel.Application]{
+ RequestConverter: converter.ApplicationDataModelFromVersioned,
+ ResponseConverter: converter.ApplicationDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.Application]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Application]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Application],
+ app_ctrl.CreateAppScopedNamespace,
+ },
+ },
+ Patch: builder.Operation[datamodel.Application]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Application]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Application],
+ app_ctrl.CreateAppScopedNamespace,
+ },
+ },
+ })
+
+ _ = ns.AddResource("httpRoutes", &builder.ResourceOption[*datamodel.HTTPRoute, datamodel.HTTPRoute]{
+ RequestConverter: converter.HTTPRouteDataModelFromVersioned,
+ ResponseConverter: converter.HTTPRouteDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.HTTPRoute]{
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ Patch: builder.Operation[datamodel.HTTPRoute]{
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ })
+
+ _ = ns.AddResource("containers", &builder.ResourceOption[*datamodel.ContainerResource, datamodel.ContainerResource]{
+ RequestConverter: converter.ContainerDataModelFromVersioned,
+ ResponseConverter: converter.ContainerDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.ContainerResource]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.ContainerResource]{
+ rp_frontend.PrepareRadiusResource[*datamodel.ContainerResource],
+ ctr_ctrl.ValidateAndMutateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ Patch: builder.Operation[datamodel.ContainerResource]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.ContainerResource]{
+ rp_frontend.PrepareRadiusResource[*datamodel.ContainerResource],
+ ctr_ctrl.ValidateAndMutateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ })
+
+ _ = ns.AddResource("gateways", &builder.ResourceOption[*datamodel.Gateway, datamodel.Gateway]{
+ RequestConverter: converter.GatewayDataModelFromVersioned,
+ ResponseConverter: converter.GatewayDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.Gateway]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Gateway]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Gateway],
+ gw_ctrl.ValidateAndMutateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ Patch: builder.Operation[datamodel.Gateway]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Gateway]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Gateway],
+ gw_ctrl.ValidateAndMutateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ })
+
+ _ = ns.AddResource("volumes", &builder.ResourceOption[*datamodel.VolumeResource, datamodel.VolumeResource]{
+ RequestConverter: converter.VolumeResourceModelFromVersioned,
+ ResponseConverter: converter.VolumeResourceModelToVersioned,
+
+ Put: builder.Operation[datamodel.VolumeResource]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.VolumeResource]{
+ rp_frontend.PrepareRadiusResource[*datamodel.VolumeResource],
+ vol_ctrl.ValidateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ Patch: builder.Operation[datamodel.VolumeResource]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.VolumeResource]{
+ rp_frontend.PrepareRadiusResource[*datamodel.VolumeResource],
+ vol_ctrl.ValidateRequest,
+ },
+ AsyncJobController: backend_ctrl.NewCreateOrUpdateResource,
+ },
+ })
+
+ _ = ns.AddResource("secretStores", &builder.ResourceOption[*datamodel.SecretStore, datamodel.SecretStore]{
+ RequestConverter: converter.SecretStoreModelFromVersioned,
+ ResponseConverter: converter.SecretStoreModelToVersioned,
+
+ Put: builder.Operation[datamodel.SecretStore]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.SecretStore]{
+ rp_frontend.PrepareRadiusResource[*datamodel.SecretStore],
+ secret_ctrl.ValidateAndMutateRequest,
+ secret_ctrl.UpsertSecret,
+ },
+ },
+ Patch: builder.Operation[datamodel.SecretStore]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.SecretStore]{
+ rp_frontend.PrepareRadiusResource[*datamodel.SecretStore],
+ secret_ctrl.ValidateAndMutateRequest,
+ secret_ctrl.UpsertSecret,
+ },
+ },
+ Delete: builder.Operation[datamodel.SecretStore]{
+ DeleteFilters: []apictrl.DeleteFilter[datamodel.SecretStore]{
+ secret_ctrl.DeleteRadiusSecret,
+ },
+ },
+ Custom: map[string]builder.Operation[datamodel.SecretStore]{
+ "listsecrets": {
+ APIController: secret_ctrl.NewListSecrets,
+ },
+ },
+ })
+
+ _ = ns.AddResource("extenders", &builder.ResourceOption[*datamodel.Extender, datamodel.Extender]{
+ RequestConverter: converter.ExtenderDataModelFromVersioned,
+ ResponseConverter: converter.ExtenderDataModelToVersioned,
+
+ Put: builder.Operation[datamodel.Extender]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Extender]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Extender],
+ },
+ AsyncJobController: func(options asyncctrl.Options) (asyncctrl.Controller, error) {
+ return pr_ctrl.NewCreateOrUpdateResource(options, &ext_processor.Processor{}, recipeControllerConfig.Engine, recipeControllerConfig.ResourceClient, recipeControllerConfig.ConfigLoader)
+ },
+ },
+ Patch: builder.Operation[datamodel.Extender]{
+ UpdateFilters: []apictrl.UpdateFilter[datamodel.Extender]{
+ rp_frontend.PrepareRadiusResource[*datamodel.Extender],
+ },
+ },
+ Delete: builder.Operation[datamodel.Extender]{
+ AsyncJobController: func(options asyncctrl.Options) (asyncctrl.Controller, error) {
+ return pr_ctrl.NewDeleteResource(options, &ext_processor.Processor{}, recipeControllerConfig.Engine, recipeControllerConfig.ConfigLoader)
+ },
+ },
+ Custom: map[string]builder.Operation[datamodel.Extender]{
+ "listsecrets": {
+ APIController: ext_ctrl.NewListSecretsExtender,
+ },
+ },
+ })
+
+ // Optional
+ ns.SetAvailableOperations(operationList)
+
+ return ns
+}
diff --git a/pkg/corerp/setup/setup_test.go b/pkg/corerp/setup/setup_test.go
new file mode 100644
index 0000000000..f6817359e0
--- /dev/null
+++ b/pkg/corerp/setup/setup_test.go
@@ -0,0 +1,253 @@
+/*
+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 setup
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/require"
+
+ v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
+ "github.com/radius-project/radius/pkg/armrpc/builder"
+ apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/rpctest"
+ "github.com/radius-project/radius/pkg/recipes/controllerconfig"
+ "github.com/radius-project/radius/pkg/ucp/dataprovider"
+ "github.com/radius-project/radius/pkg/ucp/store"
+
+ app_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/applications"
+ ctr_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/containers"
+ env_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/environments"
+ gtwy_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/gateways"
+ hrt_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/httproutes"
+ secret_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/secretstores"
+ vol_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/volumes"
+)
+
+var handlerTests = []rpctest.HandlerTestSpec{
+ {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/applications",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/applications",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/applications/app0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/applications/app0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/applications/app0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: app_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/applications/app0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/containers",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/containers",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: ctr_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/containers/ctr0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/environments",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments/env0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments/env0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments/env0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments/env0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: env_ctrl.ResourceTypeName, Method: "ACTIONGETMETADATA"},
+ Path: "/resourcegroups/testrg/providers/applications.core/environments/env0/getmetadata",
+ Method: http.MethodPost,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/gateways",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/gateways",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: gtwy_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/gateways/gateway0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/httproutes",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/httproutes",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: hrt_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/httproutes/hrt0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/secretstores",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: secret_ctrl.ResourceTypeName, Method: "ACTIONLISTSECRETS"},
+ Path: "/resourcegroups/testrg/providers/applications.core/secretstores/secret0/listsecrets",
+ Method: http.MethodPost,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPlaneScopeList},
+ Path: "/providers/applications.core/volumes",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationList},
+ Path: "/resourcegroups/testrg/providers/applications.core/volumes",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationGet},
+ Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPut},
+ Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0",
+ Method: http.MethodPut,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationPatch},
+ Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0",
+ Method: http.MethodPatch,
+ }, {
+ OperationType: v1.OperationType{Type: vol_ctrl.ResourceTypeName, Method: v1.OperationDelete},
+ Path: "/resourcegroups/testrg/providers/applications.core/volumes/volume0",
+ Method: http.MethodDelete,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet},
+ Path: "/providers/applications.core/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000",
+ Method: http.MethodGet,
+ }, {
+ OperationType: v1.OperationType{Type: "Applications.Core/operationStatuses", Method: v1.OperationGet},
+ Path: "/providers/applications.core/locations/global/operationresults/00000000-0000-0000-0000-000000000000",
+ Method: http.MethodGet,
+ },
+}
+
+func TestRouter(t *testing.T) {
+ mctrl := gomock.NewController(t)
+
+ mockSP := dataprovider.NewMockDataStorageProvider(mctrl)
+ mockSC := store.NewMockStorageClient(mctrl)
+
+ mockSC.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&store.Object{}, nil).AnyTimes()
+ mockSC.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
+ mockSP.EXPECT().GetStorageClient(gomock.Any(), gomock.Any()).Return(store.StorageClient(mockSC), nil).AnyTimes()
+
+ cfg := &controllerconfig.RecipeControllerConfig{}
+ ns := SetupNamespace(cfg)
+ nsBuilder := ns.GenerateBuilder()
+
+ rpctest.AssertRouters(t, handlerTests, "/api.ucp.dev", "/planes/radius/local", func(ctx context.Context) (chi.Router, error) {
+ r := chi.NewRouter()
+ validator, err := builder.NewOpenAPIValidator(ctx, "/api.ucp.dev", "applications.core")
+ require.NoError(t, err)
+ return r, nsBuilder.ApplyAPIHandlers(ctx, r, apictrl.Options{PathBase: "/api.ucp.dev", DataProvider: mockSP}, validator)
+ })
+}
diff --git a/pkg/kubeutil/client.go b/pkg/kubeutil/client.go
index 9b83219ff3..593a9269c5 100644
--- a/pkg/kubeutil/client.go
+++ b/pkg/kubeutil/client.go
@@ -21,12 +21,62 @@ import (
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
csidriver "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1"
)
+// Clients is a collection of Kubernetes clients.
+type Clients struct {
+ // RuntimeClient is the Kubernetes controller runtime client.
+ RuntimeClient runtimeclient.Client
+
+ // ClientSet is the Kubernetes client-go strongly-typed client.
+ ClientSet *kubernetes.Clientset
+
+ // DiscoveryClient is the Kubernetes client-go discovery client.
+ DiscoveryClient *discovery.DiscoveryClient
+
+ // DynamicClient is the Kubernetes client-go dynamic client.
+ DynamicClient dynamic.Interface
+}
+
+// NewClients creates a new Kubernetes client set and controller runtime client using the given config.
+func NewClients(config *rest.Config) (*Clients, error) {
+ c := &Clients{}
+
+ var err error
+ c.RuntimeClient, err = NewRuntimeClient(config)
+ if err != nil {
+ return nil, err
+ }
+
+ c.ClientSet, err = kubernetes.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ c.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ // Use legacy discovery client to avoid the issue of the staled GroupVersion discovery(api.ucp.dev/v1alpha3).
+ // TODO: Disable UseLegacyDiscovery once https://github.com/radius-project/radius/issues/5974 is resolved.
+ c.DiscoveryClient.UseLegacyDiscovery = true
+
+ c.DynamicClient, err = dynamic.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ return c, nil
+}
+
// NewRuntimeClient creates a new runtime client using the given config and adds the
// required resource schemes to the client.
func NewRuntimeClient(config *rest.Config) (runtimeclient.Client, error) {
diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go
index 427384ab29..ac5e5a9f87 100644
--- a/pkg/middleware/logger.go
+++ b/pkg/middleware/logger.go
@@ -24,20 +24,16 @@ import (
)
// WithLogger adds logger to the context based on the Resource ID (if present).
-func WithLogger(serviceName string) func(h http.Handler) http.Handler {
- return func(h http.Handler) http.Handler {
- fn := func(w http.ResponseWriter, r *http.Request) {
- id, err := resources.Parse(r.URL.Path)
- if err != nil {
- // This just means the request is for an ARM resource. Not an error.
- h.ServeHTTP(w, r)
- return
- }
-
- ctx := ucplog.WrapLogContext(r.Context(), ucplog.LogFieldResourceID, id.String())
- h.ServeHTTP(w, r.WithContext(ctx))
+func WithLogger(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ id, err := resources.Parse(r.URL.Path)
+ if err != nil {
+ // This just means the request is for an ARM resource. Not an error.
+ h.ServeHTTP(w, r)
+ return
}
- return http.HandlerFunc(fn)
- }
+ ctx := ucplog.WrapLogContext(r.Context(), ucplog.LogFieldResourceID, id.String())
+ h.ServeHTTP(w, r.WithContext(ctx))
+ })
}
diff --git a/pkg/portableresources/backend/controller/createorupdateresource.go b/pkg/portableresources/backend/controller/createorupdateresource.go
index d41de121d2..05aad4e8a4 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource.go
@@ -47,7 +47,7 @@ type CreateOrUpdateResource[P interface {
func NewCreateOrUpdateResource[P interface {
*T
rpv1.RadiusResourceModel
-}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, client processors.ResourceClient, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) {
+}, T any](opts ctrl.Options, processor processors.ResourceProcessor[P, T], eng engine.Engine, client processors.ResourceClient, configurationLoader configloader.ConfigurationLoader) (ctrl.Controller, error) {
return &CreateOrUpdateResource[P, T]{
ctrl.NewBaseAsyncController(opts),
processor,
diff --git a/pkg/portableresources/backend/controller/createorupdateresource_test.go b/pkg/portableresources/backend/controller/createorupdateresource_test.go
index 6a15df3873..ea65a55a08 100644
--- a/pkg/portableresources/backend/controller/createorupdateresource_test.go
+++ b/pkg/portableresources/backend/controller/createorupdateresource_test.go
@@ -33,6 +33,7 @@ import (
"github.com/radius-project/radius/pkg/portableresources/processors"
"github.com/radius-project/radius/pkg/recipes"
"github.com/radius-project/radius/pkg/recipes/configloader"
+ "github.com/radius-project/radius/pkg/recipes/controllerconfig"
"github.com/radius-project/radius/pkg/recipes/engine"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/ucp/resources"
@@ -139,7 +140,7 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
cases := []struct {
description string
- factory func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error)
+ factory func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error)
getErr error
conversionFailure bool
recipeErr error
@@ -151,8 +152,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
}{
{
"get-not-found",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
&store.ErrNotFound{ID: TestResourceID},
false,
@@ -165,8 +166,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"get-error",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
&store.ErrInvalid{},
false,
@@ -179,8 +180,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"conversion-failure",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
true,
@@ -193,8 +194,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"recipe-err",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
false,
@@ -207,8 +208,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"runtime-configuration-err",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
false,
@@ -221,8 +222,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"processor-err",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(errorProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, errorProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
false,
@@ -235,8 +236,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"save-err",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(successProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, successProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
false,
@@ -249,8 +250,8 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
},
{
"success",
- func(eng engine.Engine, client processors.ResourceClient, cfg configloader.ConfigurationLoader, options ctrl.Options) (ctrl.Controller, error) {
- return NewCreateOrUpdateResource(successProcessorReference, eng, client, cfg, options)
+ func(recipeCfg *controllerconfig.RecipeControllerConfig, options ctrl.Options) (ctrl.Controller, error) {
+ return NewCreateOrUpdateResource(options, successProcessorReference, recipeCfg.Engine, recipeCfg.ResourceClient, recipeCfg.ConfigLoader)
},
nil,
false,
@@ -403,7 +404,13 @@ func TestCreateOrUpdateResource_Run(t *testing.T) {
StorageClient: msc,
}
- genCtrl, err := tt.factory(eng, client, cfg, opts)
+ recipeCfg := &controllerconfig.RecipeControllerConfig{
+ Engine: eng,
+ ResourceClient: client,
+ ConfigLoader: cfg,
+ }
+
+ genCtrl, err := tt.factory(recipeCfg, opts)
require.NoError(t, err)
res, err := genCtrl.Run(context.Background(), req)
diff --git a/pkg/portableresources/backend/controller/deleteresource.go b/pkg/portableresources/backend/controller/deleteresource.go
index d33ed3ea4f..67d374b25a 100644
--- a/pkg/portableresources/backend/controller/deleteresource.go
+++ b/pkg/portableresources/backend/controller/deleteresource.go
@@ -45,7 +45,7 @@ type DeleteResource[P interface {
func NewDeleteResource[P interface {
*T
rpv1.RadiusResourceModel
-}, T any](processor processors.ResourceProcessor[P, T], eng engine.Engine, configurationLoader configloader.ConfigurationLoader, opts ctrl.Options) (ctrl.Controller, error) {
+}, T any](opts ctrl.Options, processor processors.ResourceProcessor[P, T], eng engine.Engine, configurationLoader configloader.ConfigurationLoader) (ctrl.Controller, error) {
return &DeleteResource[P, T]{
ctrl.NewBaseAsyncController(opts),
processor,
diff --git a/pkg/portableresources/backend/controller/deleteresource_test.go b/pkg/portableresources/backend/controller/deleteresource_test.go
index e101f56525..2fc612b3da 100644
--- a/pkg/portableresources/backend/controller/deleteresource_test.go
+++ b/pkg/portableresources/backend/controller/deleteresource_test.go
@@ -151,7 +151,7 @@ func TestDeleteResourceRun_20220315PrivatePreview(t *testing.T) {
StorageClient: msc,
}
- ctrl, err := NewDeleteResource(successProcessorReference, eng, configLoader, opts)
+ ctrl, err := NewDeleteResource(opts, successProcessorReference, eng, configLoader)
require.NoError(t, err)
_, err = ctrl.Run(context.Background(), req)
diff --git a/pkg/portableresources/backend/service.go b/pkg/portableresources/backend/service.go
index 440765cc94..808fed7e60 100644
--- a/pkg/portableresources/backend/service.go
+++ b/pkg/portableresources/backend/service.go
@@ -23,7 +23,6 @@ import (
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
"github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
"github.com/radius-project/radius/pkg/armrpc/hostoptions"
- aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials"
dapr_dm "github.com/radius-project/radius/pkg/daprrp/datamodel"
"github.com/radius-project/radius/pkg/daprrp/processors/pubsubbrokers"
"github.com/radius-project/radius/pkg/daprrp/processors/secretstores"
@@ -32,23 +31,16 @@ import (
mongo_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/mongodatabases"
redis_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/rediscaches"
sql_prc "github.com/radius-project/radius/pkg/datastoresrp/processors/sqldatabases"
+ "github.com/radius-project/radius/pkg/kubeutil"
msg_dm "github.com/radius-project/radius/pkg/messagingrp/datamodel"
"github.com/radius-project/radius/pkg/messagingrp/processors/rabbitmqqueues"
"github.com/radius-project/radius/pkg/portableresources"
"github.com/radius-project/radius/pkg/portableresources/frontend/handler"
- "github.com/radius-project/radius/pkg/portableresources/processors"
- "github.com/radius-project/radius/pkg/recipes"
- "github.com/radius-project/radius/pkg/recipes/configloader"
- "github.com/radius-project/radius/pkg/recipes/driver"
- "github.com/radius-project/radius/pkg/recipes/engine"
- "github.com/radius-project/radius/pkg/sdk"
- "github.com/radius-project/radius/pkg/sdk/clients"
+ "github.com/radius-project/radius/pkg/recipes/controllerconfig"
ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
backend_ctrl "github.com/radius-project/radius/pkg/portableresources/backend/controller"
-
- "github.com/radius-project/radius/pkg/ucp/secret/provider"
)
type Service struct {
@@ -76,29 +68,19 @@ func (s *Service) Run(ctx context.Context) error {
return err
}
- client := processors.NewResourceClient(s.Options.Arm, s.Options.UCPConnection, s.KubeClient, s.KubeDiscoveryClient)
- clientOptions := sdk.NewClientOptions(s.Options.UCPConnection)
+ k8s, err := kubeutil.NewClients(s.Options.K8sConfig)
+ if err != nil {
+ return fmt.Errorf("failed to initialize kubernetes client: %w", err)
+ }
- deploymentEngineClient, err := clients.NewResourceDeploymentsClient(&clients.Options{
- Cred: &aztoken.AnonymousCredential{},
- BaseURI: s.Options.UCPConnection.Endpoint(),
- ARMClientOptions: clientOptions,
- })
+ recipeControllerConfig, err := controllerconfig.New(s.Options)
if err != nil {
return err
}
- configLoader := configloader.NewEnvironmentLoader(clientOptions)
- engine := engine.NewEngine(engine.Options{
- ConfigurationLoader: configLoader,
- Drivers: map[string]driver.Driver{
- recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, deploymentEngineClient, client),
- recipes.TemplateKindTerraform: driver.NewTerraformDriver(s.Options.UCPConnection, provider.NewSecretProvider(s.Options.Config.SecretProvider),
- driver.TerraformOptions{
- Path: s.Options.Config.Terraform.Path,
- }, s.KubeClientSet),
- },
- })
+ engine := recipeControllerConfig.Engine
+ client := recipeControllerConfig.ResourceClient
+ configLoader := recipeControllerConfig.ConfigLoader
// resourceTypes is the array that holds resource types that needs async processing.
// We use this array to register backend controllers for each resource.
@@ -111,84 +93,84 @@ func (s *Service) Run(ctx context.Context) error {
portableresources.RabbitMQQueuesResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &rabbitmqqueues.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, client, configLoader, options)
+ return backend_ctrl.NewCreateOrUpdateResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &rabbitmqqueues.Processor{}
- return backend_ctrl.NewDeleteResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](processor, engine, configLoader, options)
+ return backend_ctrl.NewDeleteResource[*msg_dm.RabbitMQQueue, msg_dm.RabbitMQQueue](options, processor, engine, configLoader)
},
},
{
portableresources.DaprStateStoresResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &statestores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, client, configLoader, options)
+ processor := &statestores.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &statestores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewDeleteResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](processor, engine, configLoader, options)
+ processor := &statestores.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprStateStore, dapr_dm.DaprStateStore](options, processor, engine, configLoader)
},
},
{
portableresources.DaprSecretStoresResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &secretstores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, client, configLoader, options)
+ processor := &secretstores.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &secretstores.Processor{Client: s.KubeClient}
- return backend_ctrl.NewDeleteResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](processor, engine, configLoader, options)
+ processor := &secretstores.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprSecretStore, dapr_dm.DaprSecretStore](options, processor, engine, configLoader)
},
},
{
portableresources.DaprPubSubBrokersResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &pubsubbrokers.Processor{Client: s.KubeClient}
- return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, client, configLoader, options)
+ processor := &pubsubbrokers.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewCreateOrUpdateResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
- processor := &pubsubbrokers.Processor{Client: s.KubeClient}
- return backend_ctrl.NewDeleteResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](processor, engine, configLoader, options)
+ processor := &pubsubbrokers.Processor{Client: k8s.RuntimeClient}
+ return backend_ctrl.NewDeleteResource[*dapr_dm.DaprPubSubBroker, dapr_dm.DaprPubSubBroker](options, processor, engine, configLoader)
},
},
{
portableresources.MongoDatabasesResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &mongo_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, client, configLoader, options)
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &mongo_prc.Processor{}
- return backend_ctrl.NewDeleteResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](processor, engine, configLoader, options)
+ return backend_ctrl.NewDeleteResource[*ds_dm.MongoDatabase, ds_dm.MongoDatabase](options, processor, engine, configLoader)
},
},
{
portableresources.RedisCachesResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &redis_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, client, configLoader, options)
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.RedisCache, ds_dm.RedisCache](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &redis_prc.Processor{}
- return backend_ctrl.NewDeleteResource[*ds_dm.RedisCache, ds_dm.RedisCache](processor, engine, configLoader, options)
+ return backend_ctrl.NewDeleteResource[*ds_dm.RedisCache, ds_dm.RedisCache](options, processor, engine, configLoader)
},
},
{
portableresources.SqlDatabasesResourceType,
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &sql_prc.Processor{}
- return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, client, configLoader, options)
+ return backend_ctrl.NewCreateOrUpdateResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](options, processor, engine, client, configLoader)
},
func(options ctrl.Options) (ctrl.Controller, error) {
processor := &sql_prc.Processor{}
- return backend_ctrl.NewDeleteResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](processor, engine, configLoader, options)
+ return backend_ctrl.NewDeleteResource[*ds_dm.SqlDatabase, ds_dm.SqlDatabase](options, processor, engine, configLoader)
},
},
}
opts := ctrl.Options{
DataProvider: s.StorageProvider,
- KubeClient: s.KubeClient,
+ KubeClient: k8s.RuntimeClient,
}
for _, rt := range resourceTypes {
@@ -197,13 +179,11 @@ func (s *Service) Run(ctx context.Context) error {
if err != nil {
return err
}
-
err = s.Controllers.Register(ctx, rt.TypeName, v1.OperationPut, rt.CreatePutController, opts)
if err != nil {
return err
}
}
-
workerOpts := worker.Options{}
if s.Options.Config.WorkerServer != nil {
if s.Options.Config.WorkerServer.MaxOperationConcurrency != nil {
diff --git a/pkg/portableresources/frontend/handler/routes_test.go b/pkg/portableresources/frontend/handler/routes_test.go
index 29802a4bf4..ccc8f96a65 100644
--- a/pkg/portableresources/frontend/handler/routes_test.go
+++ b/pkg/portableresources/frontend/handler/routes_test.go
@@ -219,20 +219,20 @@ var handlerTests = []rpctest.HandlerTestSpec{
Path: "/resourcegroups/testrg/providers/applications.datastores/sqldatabases/sql/listsecrets",
Method: http.MethodPost,
}, {
- OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGetOperationStatuses},
+ OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGet},
Path: "/providers/applications.messaging/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
}, {
- OperationType: v1.OperationType{Type: "Applications.Messaging/operationStatuses", Method: v1.OperationGetOperationResult},
+ OperationType: v1.OperationType{Type: "Applications.Messaging/operationResults", Method: v1.OperationGet},
Path: "/providers/applications.messaging/locations/global/operationresults/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
},
{
- OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGetOperationStatuses},
+ OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGet},
Path: "/providers/applications.dapr/locations/global/operationstatuses/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
}, {
- OperationType: v1.OperationType{Type: "Applications.Dapr/operationStatuses", Method: v1.OperationGetOperationResult},
+ OperationType: v1.OperationType{Type: "Applications.Dapr/operationResults", Method: v1.OperationGet},
Path: "/providers/applications.dapr/locations/global/operationresults/00000000-0000-0000-0000-000000000000",
Method: http.MethodGet,
},
diff --git a/pkg/portableresources/frontend/service.go b/pkg/portableresources/frontend/service.go
index b83a0f4027..eb35116912 100644
--- a/pkg/portableresources/frontend/service.go
+++ b/pkg/portableresources/frontend/service.go
@@ -61,10 +61,10 @@ func (s *Service) Run(ctx context.Context) error {
}
err := s.Start(ctx, server.Options{
- Address: opts.Address,
- ProviderNamespace: s.ProviderName,
- Location: s.Options.Config.Env.RoleLocation,
- PathBase: s.Options.Config.Server.PathBase,
+ Address: opts.Address,
+ ServiceName: s.ProviderName,
+ Location: s.Options.Config.Env.RoleLocation,
+ PathBase: s.Options.Config.Server.PathBase,
// set the arm cert manager for managing client certificate
ArmCertMgr: s.ARMCertManager,
EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done
diff --git a/pkg/recipes/controllerconfig/config.go b/pkg/recipes/controllerconfig/config.go
new file mode 100644
index 0000000000..e40b93cf87
--- /dev/null
+++ b/pkg/recipes/controllerconfig/config.go
@@ -0,0 +1,85 @@
+/*
+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 controllerconfig
+
+import (
+ "github.com/radius-project/radius/pkg/armrpc/hostoptions"
+ aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials"
+ "github.com/radius-project/radius/pkg/kubeutil"
+ "github.com/radius-project/radius/pkg/portableresources/processors"
+ "github.com/radius-project/radius/pkg/recipes"
+ "github.com/radius-project/radius/pkg/recipes/configloader"
+ "github.com/radius-project/radius/pkg/recipes/driver"
+ "github.com/radius-project/radius/pkg/recipes/engine"
+ "github.com/radius-project/radius/pkg/sdk"
+ "github.com/radius-project/radius/pkg/sdk/clients"
+ "github.com/radius-project/radius/pkg/ucp/secret/provider"
+)
+
+// RecipeControllerConfig is the configuration for the controllers which uses recipe.
+type RecipeControllerConfig struct {
+ // K8sClients is the collections of Kubernetes clients.
+ K8sClients *kubeutil.Clients
+
+ // ResourceClient is a client used by resource processors for interacting with UCP resources.
+ ResourceClient processors.ResourceClient
+
+ // ConfigLoader is the configuration loader.
+ ConfigLoader configloader.ConfigurationLoader
+
+ // DeploymentEngineClient is the client for interacting with the deployment engine.
+ DeploymentEngineClient *clients.ResourceDeploymentsClient
+
+ // Engine is the engine for executing recipes.
+ Engine engine.Engine
+}
+
+// New creates a new RecipeControllerConfig instance with the given host options.
+func New(options hostoptions.HostOptions) (*RecipeControllerConfig, error) {
+ cfg := &RecipeControllerConfig{}
+ var err error
+ cfg.K8sClients, err = kubeutil.NewClients(options.K8sConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ cfg.ResourceClient = processors.NewResourceClient(options.Arm, options.UCPConnection, cfg.K8sClients.RuntimeClient, cfg.K8sClients.DiscoveryClient)
+ clientOptions := sdk.NewClientOptions(options.UCPConnection)
+
+ cfg.DeploymentEngineClient, err = clients.NewResourceDeploymentsClient(&clients.Options{
+ Cred: &aztoken.AnonymousCredential{},
+ BaseURI: options.UCPConnection.Endpoint(),
+ ARMClientOptions: sdk.NewClientOptions(options.UCPConnection),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ cfg.ConfigLoader = configloader.NewEnvironmentLoader(clientOptions)
+ cfg.Engine = engine.NewEngine(engine.Options{
+ ConfigurationLoader: cfg.ConfigLoader,
+ Drivers: map[string]driver.Driver{
+ recipes.TemplateKindBicep: driver.NewBicepDriver(clientOptions, cfg.DeploymentEngineClient, cfg.ResourceClient),
+ recipes.TemplateKindTerraform: driver.NewTerraformDriver(options.UCPConnection, provider.NewSecretProvider(options.Config.SecretProvider),
+ driver.TerraformOptions{
+ Path: options.Config.Terraform.Path,
+ }, cfg.K8sClients.ClientSet),
+ },
+ })
+
+ return cfg, nil
+}
diff --git a/pkg/server/apiservice.go b/pkg/server/apiservice.go
new file mode 100644
index 0000000000..b92d6fe2f2
--- /dev/null
+++ b/pkg/server/apiservice.go
@@ -0,0 +1,89 @@
+/*
+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 server
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/go-chi/chi/v5"
+
+ "github.com/radius-project/radius/pkg/armrpc/builder"
+ apictrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
+ "github.com/radius-project/radius/pkg/armrpc/frontend/server"
+ "github.com/radius-project/radius/pkg/armrpc/hostoptions"
+)
+
+// APIService is the restful API server for Radius Resource Provider.
+type APIService struct {
+ server.Service
+
+ handlerBuilder []builder.Builder
+}
+
+// NewAPIService creates a new instance of APIService.
+func NewAPIService(options hostoptions.HostOptions, builder []builder.Builder) *APIService {
+ return &APIService{
+ Service: server.Service{
+ ProviderName: "radius",
+ Options: options,
+ },
+ handlerBuilder: builder,
+ }
+}
+
+// Name returns the name of the service.
+func (s *APIService) Name() string {
+ return "radiusapi"
+}
+
+// Run starts the service.
+func (s *APIService) Run(ctx context.Context) error {
+ if err := s.Init(ctx); err != nil {
+ return err
+ }
+
+ address := fmt.Sprintf("%s:%d", s.Options.Config.Server.Host, s.Options.Config.Server.Port)
+ return s.Start(ctx, server.Options{
+ Location: s.Options.Config.Env.RoleLocation,
+ Address: address,
+ PathBase: s.Options.Config.Server.PathBase,
+ Configure: func(r chi.Router) error {
+ for _, b := range s.handlerBuilder {
+ opts := apictrl.Options{
+ PathBase: s.Options.Config.Server.PathBase,
+ DataProvider: s.StorageProvider,
+ KubeClient: s.KubeClient,
+ StatusManager: s.OperationStatusManager,
+ }
+
+ validator, err := builder.NewOpenAPIValidator(ctx, opts.PathBase, b.Namespace())
+ if err != nil {
+ panic(err)
+ }
+
+ if err := b.ApplyAPIHandlers(ctx, r, opts, validator); err != nil {
+ panic(err)
+ }
+ }
+ return nil
+ },
+ // set the arm cert manager for managing client certificate
+ ArmCertMgr: s.ARMCertManager,
+ EnableArmAuth: s.Options.Config.Server.EnableArmAuth, // when enabled the client cert validation will be done
+ })
+}
diff --git a/pkg/server/asyncworker.go b/pkg/server/asyncworker.go
new file mode 100644
index 0000000000..15dfb3850d
--- /dev/null
+++ b/pkg/server/asyncworker.go
@@ -0,0 +1,97 @@
+/*
+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 server
+
+import (
+ "context"
+ "fmt"
+
+ ctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
+ "github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
+ "github.com/radius-project/radius/pkg/armrpc/builder"
+ "github.com/radius-project/radius/pkg/armrpc/hostoptions"
+ "github.com/radius-project/radius/pkg/corerp/backend/deployment"
+ "github.com/radius-project/radius/pkg/corerp/model"
+ "github.com/radius-project/radius/pkg/kubeutil"
+)
+
+// AsyncWorker is a service to run AsyncReqeustProcessWorker.
+type AsyncWorker struct {
+ worker.Service
+
+ handlerBuilder []builder.Builder
+}
+
+// NewAsyncWorker creates new service instance to run AsyncReqeustProcessWorker.
+func NewAsyncWorker(options hostoptions.HostOptions, builder []builder.Builder) *AsyncWorker {
+ return &AsyncWorker{
+ Service: worker.Service{
+ ProviderName: "radius",
+ Options: options,
+ },
+ handlerBuilder: builder,
+ }
+}
+
+// Name represents the service name.
+func (w *AsyncWorker) Name() string {
+ return "radiusasyncworker"
+}
+
+// Run starts the service and worker.
+func (w *AsyncWorker) Run(ctx context.Context) error {
+ if err := w.Init(ctx); err != nil {
+ return err
+ }
+
+ k8s, err := kubeutil.NewClients(w.Options.K8sConfig)
+ if err != nil {
+ return fmt.Errorf("failed to initialize kubernetes clients: %w", err)
+ }
+
+ appModel, err := model.NewApplicationModel(w.Options.Arm, k8s.RuntimeClient, k8s.ClientSet, k8s.DiscoveryClient, k8s.DynamicClient)
+ if err != nil {
+ return fmt.Errorf("failed to initialize application model: %w", err)
+ }
+
+ for _, b := range w.handlerBuilder {
+ opts := ctrl.Options{
+ DataProvider: w.StorageProvider,
+ KubeClient: k8s.RuntimeClient,
+ GetDeploymentProcessor: func() deployment.DeploymentProcessor {
+ return deployment.NewDeploymentProcessor(appModel, w.StorageProvider, k8s.RuntimeClient, k8s.ClientSet)
+ },
+ }
+
+ err := b.ApplyAsyncHandler(ctx, w.Controllers, opts)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ workerOpts := worker.Options{}
+ if w.Options.Config.WorkerServer != nil {
+ if w.Options.Config.WorkerServer.MaxOperationConcurrency != nil {
+ workerOpts.MaxOperationConcurrency = *w.Options.Config.WorkerServer.MaxOperationConcurrency
+ }
+ if w.Options.Config.WorkerServer.MaxOperationRetryCount != nil {
+ workerOpts.MaxOperationRetryCount = *w.Options.Config.WorkerServer.MaxOperationRetryCount
+ }
+ }
+
+ return w.Start(ctx, workerOpts)
+}
diff --git a/pkg/ucp/frontend/api/server.go b/pkg/ucp/frontend/api/server.go
index 7c3fa20045..78f88b04a6 100644
--- a/pkg/ucp/frontend/api/server.go
+++ b/pkg/ucp/frontend/api/server.go
@@ -117,7 +117,7 @@ func (s *Service) Initialize(ctx context.Context) (*http.Server, error) {
r := chi.NewRouter()
s.storageProvider = dataprovider.NewStorageProvider(s.options.StorageProviderOptions)
- s.queueProvider = queueprovider.New(s.options.ProviderName, s.options.QueueProviderOptions)
+ s.queueProvider = queueprovider.New(s.options.QueueProviderOptions)
s.secretProvider = secretprovider.NewSecretProvider(s.options.SecretProviderOptions)
specLoader, err := validator.LoadSpec(ctx, "ucp", swagger.SpecFilesUCP, []string{s.options.PathBase}, "")
@@ -154,7 +154,7 @@ func (s *Service) Initialize(ctx context.Context) (*http.Server, error) {
app := http.Handler(r)
app = servicecontext.ARMRequestCtx(s.options.PathBase, "global")(app)
- app = middleware.WithLogger("ucp")(app)
+ app = middleware.WithLogger(app)
app = otelhttp.NewHandler(
middleware.NormalizePath(app),
diff --git a/pkg/ucp/frontend/aws/module.go b/pkg/ucp/frontend/aws/module.go
index 49e5f31646..676b4d0966 100644
--- a/pkg/ucp/frontend/aws/module.go
+++ b/pkg/ucp/frontend/aws/module.go
@@ -23,6 +23,14 @@ import (
"github.com/radius-project/radius/pkg/validator"
)
+const (
+ // OperationTypeAWSResource is the operation status type for AWS resources.
+ OperationStatusResourceType = "System.AWS/operationStatuses"
+
+ // OperationTypeAWSResource is the operation result type for AWS resources.
+ OperationResultsResourceType = "System.AWS/operationResults"
+)
+
// NewModule creates a new AWS module.
func NewModule(options modules.Options) *Module {
m := Module{options: options}
diff --git a/pkg/ucp/frontend/aws/routes.go b/pkg/ucp/frontend/aws/routes.go
index 85cc8e609a..2c780c07b1 100644
--- a/pkg/ucp/frontend/aws/routes.go
+++ b/pkg/ucp/frontend/aws/routes.go
@@ -84,8 +84,8 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) {
{
// URLs for standard UCP resource async status result.
ParentRouter: server.NewSubrouter(baseRouter, operationResultsPath),
- Method: v1.OperationGetOperationResult,
- OperationType: &v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationResult},
+ Method: v1.OperationGet,
+ OperationType: &v1.OperationType{Type: OperationResultsResourceType, Method: v1.OperationGet},
ControllerFactory: func(opt controller.Options) (controller.Controller, error) {
return awsproxy_ctrl.NewGetAWSOperationResults(opt, m.AWSClients)
},
@@ -93,8 +93,8 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) {
{
// URLs for standard UCP resource async status.
ParentRouter: server.NewSubrouter(baseRouter, operationStatusesPath),
- Method: v1.OperationGetOperationStatuses,
- OperationType: &v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationStatuses},
+ Method: v1.OperationGet,
+ OperationType: &v1.OperationType{Type: OperationStatusResourceType, Method: v1.OperationGet},
ControllerFactory: func(opts controller.Options) (controller.Controller, error) {
return awsproxy_ctrl.NewGetAWSOperationStatuses(opts, m.AWSClients)
},
diff --git a/pkg/ucp/frontend/aws/routes_test.go b/pkg/ucp/frontend/aws/routes_test.go
index 0549bc3387..7642d2cc3d 100644
--- a/pkg/ucp/frontend/aws/routes_test.go
+++ b/pkg/ucp/frontend/aws/routes_test.go
@@ -83,11 +83,11 @@ func Test_Routes(t *testing.T) {
Method: http.MethodPost,
Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/Stream/:delete",
}, {
- OperationType: v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationResult},
+ OperationType: v1.OperationType{Type: OperationResultsResourceType, Method: v1.OperationGet},
Method: http.MethodGet,
Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/locations/global/operationResults/00000000-0000-0000-0000-000000000000",
}, {
- OperationType: v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetOperationStatuses},
+ OperationType: v1.OperationType{Type: OperationStatusResourceType, Method: v1.OperationGet},
Method: http.MethodGet,
Path: "/planes/aws/aws/accounts/0000000/regions/some-region/providers/AWS.Kinesis/locations/global/operationStatuses/00000000-0000-0000-0000-000000000000",
},
diff --git a/pkg/ucp/integrationtests/testrp/async.go b/pkg/ucp/integrationtests/testrp/async.go
index 6bf7249c52..4723b359e6 100644
--- a/pkg/ucp/integrationtests/testrp/async.go
+++ b/pkg/ucp/integrationtests/testrp/async.go
@@ -58,13 +58,10 @@ func AsyncResource(t *testing.T, ts *testserver.TestServer, rootScope string, pu
resourceType := "System.Test/testResources"
- operationStoreClient, err := ts.Clients.StorageProvider.GetStorageClient(ctx, "System.Test/operationStatuses")
- require.NoError(t, err)
-
queueClient, err := ts.Clients.QueueProvider.GetClient(ctx)
require.NoError(t, err)
- statusManager := statusmanager.New(operationStoreClient, queueClient, "System.Test", v1.LocationGlobal)
+ statusManager := statusmanager.New(ts.Clients.StorageProvider, queueClient, v1.LocationGlobal)
backendOpts := backend_ctrl.Options{
DataProvider: ts.Clients.StorageProvider,
diff --git a/pkg/ucp/integrationtests/testserver/testserver.go b/pkg/ucp/integrationtests/testserver/testserver.go
index 5fd3830d64..ecc862d00e 100644
--- a/pkg/ucp/integrationtests/testserver/testserver.go
+++ b/pkg/ucp/integrationtests/testserver/testserver.go
@@ -149,7 +149,7 @@ func StartWithMocks(t *testing.T, configureModules func(options modules.Options)
AnyTimes()
queueClient := queue.NewMockClient(ctrl)
- queueProvider := queueprovider.New("System.Resources", queueprovider.QueueProviderOptions{})
+ queueProvider := queueprovider.New(queueprovider.QueueProviderOptions{Name: "System.Resources"})
queueProvider.SetClient(queueClient)
secretClient := secret.NewMockClient(ctrl)
@@ -254,6 +254,7 @@ func StartWithETCD(t *testing.T, configureModules func(options modules.Options)
ETCD: storageOptions.ETCD,
}
queueOptions := queueprovider.QueueProviderOptions{
+ Name: "System.Resources",
Provider: queueprovider.TypeInmemory,
InMemory: &queueprovider.InMemoryQueueOptions{},
}
@@ -262,7 +263,7 @@ func StartWithETCD(t *testing.T, configureModules func(options modules.Options)
pathBase := "/" + uuid.New().String()
dataProvider := dataprovider.NewStorageProvider(storageOptions)
secretProvider := secretprovider.NewSecretProvider(secretOptions)
- queueProvider := queueprovider.New("System.Resources", queueOptions)
+ queueProvider := queueprovider.New(queueOptions)
router := chi.NewRouter()
router.Use(servicecontext.ARMRequestCtx(pathBase, "global"))
diff --git a/pkg/ucp/queue/provider/factory.go b/pkg/ucp/queue/provider/factory.go
index 7cf1bc4efd..d3e9b3923d 100644
--- a/pkg/ucp/queue/provider/factory.go
+++ b/pkg/ucp/queue/provider/factory.go
@@ -30,18 +30,18 @@ import (
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)
-type factoryFunc func(context.Context, string, QueueProviderOptions) (queue.Client, error)
+type factoryFunc func(context.Context, QueueProviderOptions) (queue.Client, error)
var clientFactory = map[QueueProviderType]factoryFunc{
TypeInmemory: initInMemory,
TypeAPIServer: initAPIServer,
}
-func initInMemory(ctx context.Context, name string, opt QueueProviderOptions) (queue.Client, error) {
- return qinmem.NewNamedQueue(name), nil
+func initInMemory(ctx context.Context, opt QueueProviderOptions) (queue.Client, error) {
+ return qinmem.NewNamedQueue(opt.Name), nil
}
-func initAPIServer(ctx context.Context, name string, opt QueueProviderOptions) (queue.Client, error) {
+func initAPIServer(ctx context.Context, opt QueueProviderOptions) (queue.Client, error) {
if opt.APIServer.Namespace == "" {
return nil, errors.New("failed to initialize APIServer client: namespace is required")
}
@@ -76,7 +76,7 @@ func initAPIServer(ctx context.Context, name string, opt QueueProviderOptions) (
}
return apiserver.New(rc, apiserver.Options{
- Name: name,
+ Name: opt.Name,
Namespace: opt.APIServer.Namespace,
})
}
diff --git a/pkg/ucp/queue/provider/options.go b/pkg/ucp/queue/provider/options.go
index 8c06d01985..116313d03a 100644
--- a/pkg/ucp/queue/provider/options.go
+++ b/pkg/ucp/queue/provider/options.go
@@ -21,6 +21,9 @@ type QueueProviderOptions struct {
// Provider configures the storage provider.
Provider QueueProviderType `yaml:"provider"`
+ // Name represents the unique name of queue.
+ Name string `yaml:"name"`
+
// InMemory represents inmemory queue client options. (Optional)
InMemory *InMemoryQueueOptions `yaml:"inMemoryQueue,omitempty"`
diff --git a/pkg/ucp/queue/provider/provider.go b/pkg/ucp/queue/provider/provider.go
index 7d331e3dc6..b22418800c 100644
--- a/pkg/ucp/queue/provider/provider.go
+++ b/pkg/ucp/queue/provider/provider.go
@@ -22,7 +22,6 @@ import (
"sync"
queue "github.com/radius-project/radius/pkg/ucp/queue/client"
- "github.com/radius-project/radius/pkg/ucp/util"
)
var (
@@ -31,7 +30,6 @@ var (
// QueueProvider is the provider to create and manage queue client.
type QueueProvider struct {
- name string
options QueueProviderOptions
queueClient queue.Client
@@ -39,9 +37,8 @@ type QueueProvider struct {
}
// New creates new QueueProvider instance.
-func New(name string, opts QueueProviderOptions) *QueueProvider {
+func New(opts QueueProviderOptions) *QueueProvider {
return &QueueProvider{
- name: util.NormalizeStringToLower(name),
queueClient: nil,
options: opts,
}
@@ -56,7 +53,7 @@ func (p *QueueProvider) GetClient(ctx context.Context) (queue.Client, error) {
err := ErrUnsupportedStorageProvider
p.once.Do(func() {
if fn, ok := clientFactory[p.options.Provider]; ok {
- p.queueClient, err = fn(ctx, p.name, p.options)
+ p.queueClient, err = fn(ctx, p.options)
}
})
diff --git a/pkg/ucp/queue/provider/provider_test.go b/pkg/ucp/queue/provider/provider_test.go
index 839108557d..1a1bb49320 100644
--- a/pkg/ucp/queue/provider/provider_test.go
+++ b/pkg/ucp/queue/provider/provider_test.go
@@ -24,7 +24,8 @@ import (
)
func TestGetClient_ValidQueue(t *testing.T) {
- p := New("Applications.Core", QueueProviderOptions{
+ p := New(QueueProviderOptions{
+ Name: "Applications.Core",
Provider: TypeInmemory,
InMemory: &InMemoryQueueOptions{},
})
@@ -38,7 +39,8 @@ func TestGetClient_ValidQueue(t *testing.T) {
}
func TestGetClient_InvalidQueue(t *testing.T) {
- p := New("Applications.Core", QueueProviderOptions{
+ p := New(QueueProviderOptions{
+ Name: "Applications.Core",
Provider: QueueProviderType("undefined"),
})
diff --git a/pkg/ucp/server/server.go b/pkg/ucp/server/server.go
index b13f2d1e93..85d9f6eab1 100644
--- a/pkg/ucp/server/server.go
+++ b/pkg/ucp/server/server.go
@@ -18,7 +18,6 @@ package server
import (
"errors"
- "flag"
"fmt"
"os"
"strings"
@@ -151,9 +150,6 @@ func NewServerOptionsFromEnvironment() (Options, error) {
// NewServer creates a new hosting.Host instance with services for API, EmbeddedETCD, Metrics, Profiler and Backend (if
// enabled) based on the given Options.
func NewServer(options *Options) (*hosting.Host, error) {
- var enableAsyncWorker bool
- flag.BoolVar(&enableAsyncWorker, "enable-asyncworker", true, "Flag to run async request process worker (for private preview and dev/test purpose).")
-
hostingServices := []hosting.Service{
api.NewService(api.ServiceOptions{
ProviderName: UCPProviderName,
@@ -191,19 +187,17 @@ func NewServer(options *Options) (*hosting.Host, error) {
hostingServices = append(hostingServices, profilerservice.NewService(profilerOptions))
}
- if enableAsyncWorker {
- backendServiceOptions := hostOpts.HostOptions{
- Config: &hostOpts.ProviderConfig{
- StorageProvider: options.StorageProviderOptions,
- SecretProvider: options.SecretProviderOptions,
- QueueProvider: options.QueueProviderOptions,
- MetricsProvider: options.MetricsProviderOptions,
- TracerProvider: options.TracerProviderOptions,
- ProfilerProvider: options.ProfilerProviderOptions,
- },
- }
- hostingServices = append(hostingServices, backend.NewService(backendServiceOptions))
+ backendServiceOptions := hostOpts.HostOptions{
+ Config: &hostOpts.ProviderConfig{
+ StorageProvider: options.StorageProviderOptions,
+ SecretProvider: options.SecretProviderOptions,
+ QueueProvider: options.QueueProviderOptions,
+ MetricsProvider: options.MetricsProviderOptions,
+ TracerProvider: options.TracerProviderOptions,
+ ProfilerProvider: options.ProfilerProviderOptions,
+ },
}
+ hostingServices = append(hostingServices, backend.NewService(backendServiceOptions))
return &hosting.Host{
Services: hostingServices,