diff --git a/.github/workflows/functional-test-cloud.yaml b/.github/workflows/functional-test-cloud.yaml index a15b0c6e22..a55c7ee5dd 100644 --- a/.github/workflows/functional-test-cloud.yaml +++ b/.github/workflows/functional-test-cloud.yaml @@ -657,6 +657,50 @@ jobs: --set global.azureWorkloadIdentity.enabled=true \ --set global.aws.irsa.enabled=true + echo "*** Verify manifests are registered ***" + rm -f registermanifest_logs.txt + # Find the pod with container "ucp" + POD_NAME=$( + kubectl get pods -n radius-system \ + -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[*].name}{"\n"}{end}' \ + | grep "ucp" \ + | head -n1 \ + | cut -d" " -f1 + ) + echo "Found ucp pod: $POD_NAME" + + if [ -z "$POD_NAME" ]; then + echo "No pod with container 'ucp' found in namespace radius-system." + exit 1 + fi + + # Poll logs for up to iterations, 30 seconds each (upto 3 minutes total) + for i in {1..6}; do + kubectl logs "$POD_NAME" -n radius-system | tee registermanifest_logs.txt > /dev/null + + # Exit on error + if grep -qi "Service initializer terminated with error" registermanifest_logs.txt; then + echo "Error found in ucp logs:" + grep -i "Service initializer terminated with error" registermanifest_logs.txt + exit 1 + fi + + # Check for success + if grep -q "Successfully registered manifests" registermanifest_logs.txt; then + echo "Successfully registered manifests - message found." + break + fi + + echo "Logs not ready, waiting 30 seconds..." + sleep 30 + done + + # Final check to ensure success message was found + if ! grep -q "Successfully registered manifests" registermanifest_logs.txt; then + echo "Manifests not registered after 3 minutes." + exit 1 + fi + echo "*** Create workspace, group and environment for test ***" rad workspace create kubernetes rad group create kind-radius @@ -678,7 +722,7 @@ jobs: rad credential register aws irsa \ --iam-role ${{ secrets.FUNC_TEST_RAD_IRSA_ROLE }} - + - uses: marocchino/sticky-pull-request-comment@v2 if: failure() && env.PR_NUMBER != '' continue-on-error: true @@ -903,4 +947,4 @@ jobs: title: `Scheduled functional test failed - Run ID: ${context.runId}`, labels: ['bug', 'test-failure'], body: `## Bug information \n\nThis issue is automatically generated if the scheduled functional test fails. The Radius functional test operates on a schedule of every 4 hours during weekdays and every 12 hours over the weekend. It's important to understand that the test may fail due to workflow infrastructure issues, like network problems, rather than the flakiness of the test itself. For the further investigation, please visit [here](${process.env.ACTION_LINK}).` - }) + }) \ No newline at end of file diff --git a/.github/workflows/functional-test-noncloud.yaml b/.github/workflows/functional-test-noncloud.yaml index 4587555765..bfccf79e05 100644 --- a/.github/workflows/functional-test-noncloud.yaml +++ b/.github/workflows/functional-test-noncloud.yaml @@ -272,6 +272,50 @@ jobs: echo "*** Installing Radius to Kubernetes ***" eval $RAD_COMMAND + echo "*** Verify manifests are registered ***" + rm -f registermanifest_logs.txt + # Find the pod with container "ucp" + POD_NAME=$( + kubectl get pods -n radius-system \ + -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[*].name}{"\n"}{end}' \ + | grep "ucp" \ + | head -n1 \ + | cut -d" " -f1 + ) + echo "Found ucp pod: $POD_NAME" + + if [ -z "$POD_NAME" ]; then + echo "No pod with container 'ucp' found in namespace radius-system." + exit 1 + fi + + # Poll logs for up to iterations, 30 seconds each (upto 3 minutes total) + for i in {1..6}; do + kubectl logs "$POD_NAME" -n radius-system | tee registermanifest_logs.txt > /dev/null + + # Exit on error + if grep -qi "Service initializer terminated with error" registermanifest_logs.txt; then + echo "Error found in ucp logs." + grep -i "Service initializer terminated with error" registermanifest_logs.txt + exit 1 + fi + + # Check for success + if grep -q "Successfully registered manifests" registermanifest_logs.txt; then + echo "Successfully registered manifests - message found." + break + fi + + echo "Logs not ready, waiting 30 seconds..." + sleep 30 + done + + # Final check to ensure success message was found + if ! grep -q "Successfully registered manifests" registermanifest_logs.txt; then + echo "Manifests not registered after 3 minutes." + exit 1 + fi + echo "*** Create workspace, group and environment for test ***" rad workspace create kubernetes rad group create kind-radius @@ -280,6 +324,8 @@ jobs: # The functional test is designed to use default namespace. So you must create the environment for default namespace. rad env create kind-radius --namespace default rad env switch kind-radius + continue-on-error: true + env: USE_CERT_FILE: "true" TEMP_CERT_DIR: ${{ steps.create-local-registry.outputs.temp-cert-dir }} diff --git a/build/docker.mk b/build/docker.mk index 7adcdcaa6b..0fe2268b30 100644 --- a/build/docker.mk +++ b/build/docker.mk @@ -17,6 +17,7 @@ DOCKER_REGISTRY?=$(shell whoami) DOCKER_TAG_VERSION?=latest IMAGE_SRC?=https://github.com/radius-project/radius +MANIFEST_DIR?=deploy/manifest/built-in-providers/self-hosted ##@ Docker Images @@ -106,6 +107,17 @@ APPS_MAP := ucpd:./deploy/images/ucpd \ testrp:./test/testrp \ magpiego:./test/magpiego +# copy_manifests copies the manifests to the output directory +.PHONY: copy-manifests +copy-manifests: + @if [ ! -d "$(MANIFEST_DIR)" ] || [ -z "$$(ls -A $(MANIFEST_DIR))" ]; then \ + echo "MANIFEST_DIR '$(MANIFEST_DIR)' does not exist or is empty"; \ + exit 1; \ + fi + @mkdir -p $(OUT_DIR)/manifest/built-in-providers/ + @echo "Copying manifests from $(MANIFEST_DIR) to $(OUT_DIR)/manifest/built-in-providers/" + @cp -v $(MANIFEST_DIR)/* $(OUT_DIR)/manifest/built-in-providers/ + # Function to extract the name and the directory of the Dockerfile from the app string define parseApp $(eval NAME := $(shell echo $(1) | cut -d: -f1)) @@ -132,7 +144,7 @@ DOCKER_PUSH_MULTI_TARGETS := $(foreach APP,$(APPS_MAP),$(eval $(call parseApp,$( # targets to build development images .PHONY: docker-build -docker-build: $(DOCKER_BUILD_TARGETS) ## Builds all Docker images. +docker-build: copy-manifests $(DOCKER_BUILD_TARGETS) ## Builds all Docker images. .PHONY: docker-push docker-push: $(DOCKER_PUSH_TARGETS) ## Pushes all Docker images (without building). @@ -140,7 +152,7 @@ docker-push: $(DOCKER_PUSH_TARGETS) ## Pushes all Docker images (without buildin # targets to build and push multi arch images. If you run this target in your machine, # ensure you have qemu and buildx installed by running make configure-buildx. .PHONY: docker-multi-arch-build -docker-multi-arch-build: $(DOCKER_BUILD_MULTI_TARGETS) ## Builds all docker images for multiple architectures. +docker-multi-arch-build: copy-manifests $(DOCKER_BUILD_MULTI_TARGETS) ## Builds all docker images for multiple architectures. .PHONY: docker-multi-arch-push -docker-multi-arch-push: $(DOCKER_PUSH_MULTI_TARGETS) ## Pushes all docker images for multiple architectures after building. +docker-multi-arch-push: copy-manifests $(DOCKER_PUSH_MULTI_TARGETS) ## Pushes all docker images for multiple architectures after building. diff --git a/cmd/ucpd/ucp-dev.yaml b/cmd/ucpd/ucp-dev.yaml index ddabe6bb0b..ae0da2ff5e 100644 --- a/cmd/ucpd/ucp-dev.yaml +++ b/cmd/ucpd/ucp-dev.yaml @@ -53,7 +53,7 @@ initialization: Microsoft.Resources: "http://localhost:5017" kind: "UCPNative" # This is the directory location which contains manifests to be registered. - manifestDirectory: "" + manifestDirectory: "manifest/built-in-providers/" identity: authMethod: default diff --git a/deploy/Chart/templates/ucp/configmaps.yaml b/deploy/Chart/templates/ucp/configmaps.yaml index 2047e006a8..1401b281b9 100644 --- a/deploy/Chart/templates/ucp/configmaps.yaml +++ b/deploy/Chart/templates/ucp/configmaps.yaml @@ -50,7 +50,7 @@ data: - id: "/planes/aws/aws" properties: kind: "AWS" - manifestDirectory: "" + manifestDirectory: "/manifest/built-in-providers" identity: authMethod: UCPCredential diff --git a/deploy/images/ucpd/Dockerfile b/deploy/images/ucpd/Dockerfile index 44ff6e5be3..b2243c39ad 100644 --- a/deploy/images/ucpd/Dockerfile +++ b/deploy/images/ucpd/Dockerfile @@ -10,6 +10,9 @@ WORKDIR / # Copy the application binary for the specified architecture COPY ./linux_${TARGETARCH:-amd64}/release/ucpd / +# Copy the manifest files for the built-in providers +COPY ./manifest/built-in-providers/ /manifest/built-in-providers/ + # Set the user to non-root (65532:65532 is the default non-root user in distroless) USER 65532:65532 diff --git a/deploy/manifest/built-in-providers/applications_core.yaml b/deploy/manifest/built-in-providers/dev/applications_core.yaml similarity index 69% rename from deploy/manifest/built-in-providers/applications_core.yaml rename to deploy/manifest/built-in-providers/dev/applications_core.yaml index d105acaf01..48fa2c996c 100644 --- a/deploy/manifest/built-in-providers/applications_core.yaml +++ b/deploy/manifest/built-in-providers/dev/applications_core.yaml @@ -1,37 +1,40 @@ name: Applications.Core +location: + global: + "http://localhost:8080" types: containers: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] applications: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] environments: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] gateways: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] secretStores: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] extenders: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] volumes: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: [] diff --git a/deploy/manifest/built-in-providers/applications_dapr.yaml b/deploy/manifest/built-in-providers/dev/applications_dapr.yaml similarity index 71% rename from deploy/manifest/built-in-providers/applications_dapr.yaml rename to deploy/manifest/built-in-providers/dev/applications_dapr.yaml index 3738e87643..57b4c9d6dc 100644 --- a/deploy/manifest/built-in-providers/applications_dapr.yaml +++ b/deploy/manifest/built-in-providers/dev/applications_dapr.yaml @@ -1,22 +1,25 @@ name: Applications.Dapr +location: + global: + "http://localhost:8080" types: configurationStores: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] pubSubBrokers: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] secretStores: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] stateStores: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/applications_datastores.yaml b/deploy/manifest/built-in-providers/dev/applications_datastores.yaml similarity index 70% rename from deploy/manifest/built-in-providers/applications_datastores.yaml rename to deploy/manifest/built-in-providers/dev/applications_datastores.yaml index b8eb7d65b7..30c78a5641 100644 --- a/deploy/manifest/built-in-providers/applications_datastores.yaml +++ b/deploy/manifest/built-in-providers/dev/applications_datastores.yaml @@ -1,17 +1,20 @@ name: Applications.Datastores +location: + global: + "http://localhost:8080" types: mongoDatabases: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] sqlDatabases: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] redisCaches: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/applications_messaging.yaml b/deploy/manifest/built-in-providers/dev/applications_messaging.yaml similarity index 62% rename from deploy/manifest/built-in-providers/applications_messaging.yaml rename to deploy/manifest/built-in-providers/dev/applications_messaging.yaml index cac03e8aa4..ba41b3a11b 100644 --- a/deploy/manifest/built-in-providers/applications_messaging.yaml +++ b/deploy/manifest/built-in-providers/dev/applications_messaging.yaml @@ -1,7 +1,10 @@ name: Applications.Messaging +location: + global: + "http://localhost:8080" types: rabbitMQQueues: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/dev/microsoft_resources.yaml b/deploy/manifest/built-in-providers/dev/microsoft_resources.yaml new file mode 100644 index 0000000000..e780319a1c --- /dev/null +++ b/deploy/manifest/built-in-providers/dev/microsoft_resources.yaml @@ -0,0 +1,12 @@ +name: Microsoft.Resources +location: + global: + "http://localhost:5017" +types: + deployments: + apiVersions: + "2020-10-01": + schema: {} + "2022-09-01": + schema: {} + capabilities: [] diff --git a/deploy/manifest/built-in-providers/microsoft_resources.yaml b/deploy/manifest/built-in-providers/microsoft_resources.yaml deleted file mode 100644 index b2c24733bb..0000000000 --- a/deploy/manifest/built-in-providers/microsoft_resources.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: Microsoft.Resources -types: - deployments: - apiVersions: - "2025-01-01-preview": - schema: {} - capabilities: [] - diff --git a/deploy/manifest/built-in-providers/self-hosted/applications_core.yaml b/deploy/manifest/built-in-providers/self-hosted/applications_core.yaml new file mode 100644 index 0000000000..e7038d710d --- /dev/null +++ b/deploy/manifest/built-in-providers/self-hosted/applications_core.yaml @@ -0,0 +1,40 @@ +name: Applications.Core +location: + global: + "http://applications-rp.radius-system:5443" +types: + containers: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] + applications: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] + environments: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] + gateways: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] + secretStores: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] + extenders: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + volumes: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: [] diff --git a/deploy/manifest/built-in-providers/self-hosted/applications_dapr.yaml b/deploy/manifest/built-in-providers/self-hosted/applications_dapr.yaml new file mode 100644 index 0000000000..01e0400565 --- /dev/null +++ b/deploy/manifest/built-in-providers/self-hosted/applications_dapr.yaml @@ -0,0 +1,25 @@ +name: Applications.Dapr +location: + global: + "http://applications-rp.radius-system:5443" +types: + configurationStores: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + pubSubBrokers: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + secretStores: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + stateStores: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/self-hosted/applications_datastores.yaml b/deploy/manifest/built-in-providers/self-hosted/applications_datastores.yaml new file mode 100644 index 0000000000..80a6b24a4b --- /dev/null +++ b/deploy/manifest/built-in-providers/self-hosted/applications_datastores.yaml @@ -0,0 +1,20 @@ +name: Applications.Datastores +location: + global: + "http://applications-rp.radius-system:5443" +types: + mongoDatabases: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + sqlDatabases: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] + redisCaches: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/self-hosted/applications_messaging.yaml b/deploy/manifest/built-in-providers/self-hosted/applications_messaging.yaml new file mode 100644 index 0000000000..b00483559f --- /dev/null +++ b/deploy/manifest/built-in-providers/self-hosted/applications_messaging.yaml @@ -0,0 +1,10 @@ +name: Applications.Messaging +location: + global: + "http://applications-rp.radius-system:5443" +types: + rabbitMQQueues: + apiVersions: + "2023-10-01-preview": + schema: {} + capabilities: ["SupportsRecipes"] diff --git a/deploy/manifest/built-in-providers/self-hosted/microsoft_resources.yaml b/deploy/manifest/built-in-providers/self-hosted/microsoft_resources.yaml new file mode 100644 index 0000000000..969a2e491e --- /dev/null +++ b/deploy/manifest/built-in-providers/self-hosted/microsoft_resources.yaml @@ -0,0 +1,12 @@ +name: Microsoft.Resources +location: + global: + "http://bicep-de.radius-system:6443" +types: + deployments: + apiVersions: + "2020-10-01": + schema: {} + "2022-09-01": + schema: {} + capabilities: [] diff --git a/pkg/cli/cmd/resourceprovider/create/testdata/valid.yaml b/pkg/cli/cmd/resourceprovider/create/testdata/valid.yaml index a6f26f7fb4..f2eee239d6 100644 --- a/pkg/cli/cmd/resourceprovider/create/testdata/valid.yaml +++ b/pkg/cli/cmd/resourceprovider/create/testdata/valid.yaml @@ -1,4 +1,7 @@ name: MyCompany.Resources +location: + global: + 'http://localhost:8080' types: testResources: apiVersions: diff --git a/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml b/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml index a6f26f7fb4..621c3926c3 100644 --- a/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml +++ b/pkg/cli/cmd/resourcetype/create/testdata/valid.yaml @@ -2,6 +2,6 @@ name: MyCompany.Resources types: testResources: apiVersions: - '2025-01-01-preview': + '2023-10-01-preview': schema: {} - capabilities: ["SupportsRecipes"] \ No newline at end of file + capabilities: ["SupportsRecipes"] diff --git a/pkg/cli/manifest/manifest.go b/pkg/cli/manifest/manifest.go index d7a8a5855e..9e112f0cf4 100644 --- a/pkg/cli/manifest/manifest.go +++ b/pkg/cli/manifest/manifest.go @@ -21,6 +21,9 @@ type ResourceProvider struct { // Name is the resource provider name. This is also the namespace of the types defined by the resource provider. Name string `yaml:"name" validate:"required,resourceProviderNamespace"` + // Location is a map of location name to address in the resource provider. + Location map[string]string `yaml:"location,omitempty"` + // Types is a map of resource types in the resource provider. Types map[string]*ResourceType `yaml:"types" validate:"dive,keys,resourceType,endkeys,required"` } diff --git a/pkg/cli/manifest/manifest_test.go b/pkg/cli/manifest/manifest_test.go index 23c11de187..431ed1fc40 100644 --- a/pkg/cli/manifest/manifest_test.go +++ b/pkg/cli/manifest/manifest_test.go @@ -25,6 +25,9 @@ import ( func TestReadFileYAML(t *testing.T) { expected := &ResourceProvider{ Name: "MyCompany.Resources", + Location: map[string]string{ + "global": "http://localhost:8080", + }, Types: map[string]*ResourceType{ "testResources": { APIVersions: map[string]*ResourceTypeAPIVersion{ diff --git a/pkg/cli/manifest/registermanifest.go b/pkg/cli/manifest/registermanifest.go index 8f785def4b..7ba5fdbe83 100644 --- a/pkg/cli/manifest/registermanifest.go +++ b/pkg/cli/manifest/registermanifest.go @@ -38,9 +38,21 @@ func RegisterFile(ctx context.Context, clientFactory *v20231001preview.ClientFac return err } - logIfEnabled(logger, "Creating resource provider %s", resourceProvider.Name) + var locationName string + var address string + + if resourceProvider.Location == nil { + locationName = v1.LocationGlobal + } else { + for locationName, address = range resourceProvider.Location { + // We support one location per resourceProvider + break + } + } + + logIfEnabled(logger, "Creating resource provider %s at location %s", resourceProvider.Name, locationName) resourceProviderPoller, err := clientFactory.NewResourceProvidersClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, v20231001preview.ResourceProviderResource{ - Location: to.Ptr(v1.LocationGlobal), + Location: to.Ptr(locationName), Properties: &v20231001preview.ResourceProviderProperties{}, }, nil) if err != nil { @@ -101,8 +113,12 @@ func RegisterFile(ctx context.Context, clientFactory *v20231001preview.ClientFac locationResource.Properties.ResourceTypes[resourceTypeName] = locationResourceType } - logIfEnabled(logger, "Creating location %s/%s", resourceProvider.Name, v1.LocationGlobal) - locationPoller, err := clientFactory.NewLocationsClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, v1.LocationGlobal, locationResource, nil) + if address != "" { + locationResource.Properties.Address = to.Ptr(address) + } + + logIfEnabled(logger, "Creating location %s/%s/%s", resourceProvider.Name, locationName, address) + locationPoller, err := clientFactory.NewLocationsClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, locationName, locationResource, nil) if err != nil { return err } @@ -167,9 +183,21 @@ func RegisterType(ctx context.Context, clientFactory *v20231001preview.ClientFac return err } + var locationName string + var address string + + if resourceProvider.Location == nil { + locationName = v1.LocationGlobal + } else { + for locationName, address = range resourceProvider.Location { + // We support one location per resourceProvider + break + } + } + resourceType, ok := resourceProvider.Types[typeName] if !ok { - return fmt.Errorf("Type %s not found in manifest file %s", typeName, filePath) + return fmt.Errorf("type %s not found in manifest file %s", typeName, filePath) } logIfEnabled(logger, "Creating resource type %s/%s", resourceProvider.Name, typeName) @@ -203,7 +231,7 @@ func RegisterType(ctx context.Context, clientFactory *v20231001preview.ClientFac } // get the existing location resource and update it with new resource type. We have to revisit this code once schema is finalized and validated. - locationResourceGetResponse, err := clientFactory.NewLocationsClient().Get(ctx, planeName, resourceProvider.Name, v1.LocationGlobal, nil) + locationResourceGetResponse, err := clientFactory.NewLocationsClient().Get(ctx, planeName, resourceProvider.Name, locationName, nil) if err != nil { return err } @@ -214,15 +242,20 @@ func RegisterType(ctx context.Context, clientFactory *v20231001preview.ClientFac } else { defaultAPIVersion = *resourceType.DefaultAPIVersion } + locationResource := locationResourceGetResponse.LocationResource + if address != "" { + locationResource.Properties.Address = to.Ptr(address) + } + locationResource.Properties.ResourceTypes[typeName] = &v20231001preview.LocationResourceType{ APIVersions: map[string]map[string]any{ defaultAPIVersion: {}, }, } - logIfEnabled(logger, "Updating location %s/%s with new resource type", resourceProvider.Name, v1.LocationGlobal) - locationPoller, err := clientFactory.NewLocationsClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, v1.LocationGlobal, locationResource, nil) + logIfEnabled(logger, "Updating location %s/%s with new resource type", resourceProvider.Name, locationName) + locationPoller, err := clientFactory.NewLocationsClient().BeginCreateOrUpdate(ctx, planeName, resourceProvider.Name, locationName, locationResource, nil) if err != nil { return err } diff --git a/pkg/cli/manifest/registermanifest_test.go b/pkg/cli/manifest/registermanifest_test.go index 8026c6b898..847939b8bc 100644 --- a/pkg/cli/manifest/registermanifest_test.go +++ b/pkg/cli/manifest/registermanifest_test.go @@ -190,7 +190,7 @@ func TestRegisterType(t *testing.T) { resourceTypeName: "testResource5", filePath: "testdata/registerdirectory/resourceprovider-valid2.yaml", expectError: true, - expectedErrorMessage: "Type testResource5 not found in manifest file testdata/registerdirectory/resourceprovider-valid2.yaml", + expectedErrorMessage: "type testResource5 not found in manifest file testdata/registerdirectory/resourceprovider-valid2.yaml", expectedResourceProvider: "", expectedResourceTypeName: "", }, diff --git a/pkg/cli/manifest/testdata/missing-required-field.json b/pkg/cli/manifest/testdata/missing-required-field.json index c6ef38a2f2..8a200cbec9 100644 --- a/pkg/cli/manifest/testdata/missing-required-field.json +++ b/pkg/cli/manifest/testdata/missing-required-field.json @@ -4,7 +4,7 @@ "apiVersions": { "2025-01-01-preview": { "schema": {}, - "capabilities": ["Recipes"] + "capabilities": ["SupportsRecipes"] } } } diff --git a/pkg/cli/manifest/testdata/registerdirectory/resourceprovider-valid2.yaml b/pkg/cli/manifest/testdata/registerdirectory/resourceprovider-valid2.yaml index a2ffd310e7..0b70586ad3 100644 --- a/pkg/cli/manifest/testdata/registerdirectory/resourceprovider-valid2.yaml +++ b/pkg/cli/manifest/testdata/registerdirectory/resourceprovider-valid2.yaml @@ -1,4 +1,7 @@ name: MyCompany2.CompanyName2 +location: + global: + 'http://localhost:8080' types: testResource3: apiVersions: diff --git a/pkg/cli/manifest/testdata/valid.yaml b/pkg/cli/manifest/testdata/valid.yaml index a6f26f7fb4..f2eee239d6 100644 --- a/pkg/cli/manifest/testdata/valid.yaml +++ b/pkg/cli/manifest/testdata/valid.yaml @@ -1,4 +1,7 @@ name: MyCompany.Resources +location: + global: + 'http://localhost:8080' types: testResources: apiVersions: diff --git a/pkg/ucp/backend/controller/resourcegroups/trackedresourceprocess_test.go b/pkg/ucp/backend/controller/resourcegroups/trackedresourceprocess_test.go index 9ee2929d5f..2d9deeeb14 100644 --- a/pkg/ucp/backend/controller/resourcegroups/trackedresourceprocess_test.go +++ b/pkg/ucp/backend/controller/resourcegroups/trackedresourceprocess_test.go @@ -24,6 +24,7 @@ import ( v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller" "github.com/radius-project/radius/pkg/components/database" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/pkg/ucp/datamodel" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/trackedresource" @@ -47,10 +48,15 @@ func Test_Run(t *testing.T) { id := resources.MustParse("/planes/test/local/resourceGroups/test-rg/providers/Applications.Test/testResources/my-resource") trackingID := trackedresource.IDFor(id) + data := datamodel.GenericResourceFromID(id, trackingID) + data.Properties.APIVersion = "2025-01-01" resourceTypeID, err := datamodel.ResourceTypeIDFromResourceID(id) require.NoError(t, err) + locationID, err := datamodel.ResourceProviderLocationIDFromResourceID(id, "global") + require.NoError(t, err) + plane := datamodel.RadiusPlane{ Properties: datamodel.RadiusPlaneProperties{ ResourceProviders: map[string]string{ @@ -58,8 +64,42 @@ func Test_Run(t *testing.T) { }, }, } - resourceGroup := datamodel.ResourceGroup{} - data := datamodel.GenericResourceFromID(id, trackingID) + resourceGroup := &datamodel.ResourceGroup{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + ID: id.RootScope(), + }, + }, + } + + resourceTypeResource := &datamodel.ResourceType{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + Name: "testResources", + ID: resourceTypeID.String(), + }, + }, + Properties: datamodel.ResourceTypeProperties{}, + } + + locationResource := &datamodel.Location{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + Name: "global", + ID: locationID.String(), + }, + }, + Properties: datamodel.LocationProperties{ + Address: to.Ptr("https://localhost:1234"), + ResourceTypes: map[string]datamodel.LocationResourceTypeConfiguration{ + "testResources": { + APIVersions: map[string]datamodel.LocationAPIVersionConfiguration{ + "2025-01-01": {}, + }, + }, + }, + }, + } // Most of the heavy lifting is done by the updater. We just need to test that we're calling it correctly. t.Run("Success", func(t *testing.T) { @@ -70,17 +110,21 @@ func Test_Run(t *testing.T) { Return(&database.Object{Data: data}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+trackingID.PlaneNamespace(), gomock.Any()). + Get(gomock.Any(), trackingID.PlaneScope(), gomock.Any()). Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), trackingID.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + result, err := pc.Run(testcontext.New(t), &controller.Request{ResourceID: trackingID.String()}) require.Equal(t, controller.Result{}, result) require.NoError(t, err) @@ -94,17 +138,21 @@ func Test_Run(t *testing.T) { Return(&database.Object{Data: data}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+trackingID.PlaneNamespace(), gomock.Any()). + Get(gomock.Any(), trackingID.PlaneScope(), gomock.Any()). Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), trackingID.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + // Force a retry. updater.Result = &trackedresource.InProgressErr{} diff --git a/pkg/ucp/datamodel/radiusplane.go b/pkg/ucp/datamodel/radiusplane.go index 4c47a3d2a8..08243b1989 100644 --- a/pkg/ucp/datamodel/radiusplane.go +++ b/pkg/ucp/datamodel/radiusplane.go @@ -17,8 +17,6 @@ limitations under the License. package datamodel import ( - "strings" - v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" ) @@ -46,15 +44,3 @@ type RadiusPlane struct { func (p RadiusPlane) ResourceTypeName() string { return p.Type } - -// LookupResourceProvider checks if the input provider is in the list of configured providers. -func (plane RadiusPlane) LookupResourceProvider(key string) string { - var value string - for k, v := range plane.Properties.ResourceProviders { - if strings.EqualFold(k, key) { - value = v - break - } - } - return value -} diff --git a/pkg/ucp/frontend/controller/radius/proxy_test.go b/pkg/ucp/frontend/controller/radius/proxy_test.go index 063105e599..108f543731 100644 --- a/pkg/ucp/frontend/controller/radius/proxy_test.go +++ b/pkg/ucp/frontend/controller/radius/proxy_test.go @@ -29,6 +29,7 @@ import ( "github.com/radius-project/radius/pkg/armrpc/frontend/controller" "github.com/radius-project/radius/pkg/armrpc/rest" "github.com/radius-project/radius/pkg/components/database" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/pkg/ucp/datamodel" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/trackedresource" @@ -68,11 +69,12 @@ func createController(t *testing.T) (*ProxyController, *database.MockClient, *mo func Test_Run(t *testing.T) { id := resources.MustParse("/planes/test/local/resourceGroups/test-rg/providers/Applications.Test/testResources/my-resource") - // This test covers the legacy (pre-UDT) behavior for looking up the downstream URL. Update - // this when the old behavior is removed. resourceTypeID, err := datamodel.ResourceTypeIDFromResourceID(id) require.NoError(t, err) + locationID, err := datamodel.ResourceProviderLocationIDFromResourceID(id, "global") + require.NoError(t, err) + plane := datamodel.RadiusPlane{ Properties: datamodel.RadiusPlaneProperties{ ResourceProviders: map[string]string{ @@ -80,7 +82,42 @@ func Test_Run(t *testing.T) { }, }, } - resourceGroup := datamodel.ResourceGroup{} + resourceGroup := &datamodel.ResourceGroup{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + ID: id.RootScope(), + }, + }, + } + + resourceTypeResource := &datamodel.ResourceType{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + Name: "testResources", + ID: resourceTypeID.String(), + }, + }, + Properties: datamodel.ResourceTypeProperties{}, + } + + locationResource := &datamodel.Location{ + BaseResource: v1.BaseResource{ + TrackedResource: v1.TrackedResource{ + Name: "global", + ID: locationID.String(), + }, + }, + Properties: datamodel.LocationProperties{ + Address: to.Ptr("https://localhost:1234"), + ResourceTypes: map[string]datamodel.LocationResourceTypeConfiguration{ + "testResources": { + APIVersions: map[string]datamodel.LocationAPIVersionConfiguration{ + "2025-01-01": {}, + }, + }, + }, + }, + } t.Run("success (non-tracked)", func(t *testing.T) { p, databaseClient, _, roundTripper, _ := createController(t) @@ -98,17 +135,21 @@ func Test_Run(t *testing.T) { req := httptest.NewRequest(http.MethodGet, id.String()+"?api-version="+apiVersion, nil) databaseClient.EXPECT(). - Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Get(gomock.Any(), id.PlaneScope(), gomock.Any()). + Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+id.PlaneNamespace(), gomock.Any()). - Return(&database.Object{Data: plane}, nil).Times(1) + Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), id.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + downstreamResponse := httptest.NewRecorder() downstreamResponse.WriteHeader(http.StatusOK) roundTripper.Response = downstreamResponse.Result() @@ -134,17 +175,21 @@ func Test_Run(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, id.String()+"?api-version="+apiVersion, nil) databaseClient.EXPECT(). - Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Get(gomock.Any(), id.PlaneScope(), gomock.Any()). + Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+id.PlaneNamespace(), gomock.Any()). - Return(&database.Object{Data: plane}, nil).Times(1) + Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), id.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + downstreamResponse := httptest.NewRecorder() downstreamResponse.WriteHeader(http.StatusOK) roundTripper.Response = downstreamResponse.Result() @@ -173,17 +218,21 @@ func Test_Run(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, id.String()+"?api-version="+apiVersion, nil) databaseClient.EXPECT(). - Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Get(gomock.Any(), id.PlaneScope(), gomock.Any()). + Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+id.PlaneNamespace(), gomock.Any()). - Return(&database.Object{Data: plane}, nil).Times(1) + Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), id.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + // Tracking entry created databaseClient.EXPECT(). Get(gomock.Any(), gomock.Any(), gomock.Any()). @@ -224,17 +273,21 @@ func Test_Run(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, id.String()+"?api-version="+apiVersion, nil) databaseClient.EXPECT(). - Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). - Return(nil, &database.ErrNotFound{}).Times(1) + Get(gomock.Any(), id.PlaneScope(), gomock.Any()). + Return(&database.Object{Data: plane}, nil).Times(1) databaseClient.EXPECT(). - Get(gomock.Any(), "/planes/"+id.PlaneNamespace(), gomock.Any()). - Return(&database.Object{Data: plane}, nil).Times(1) + Get(gomock.Any(), resourceTypeID.String(), gomock.Any()). + Return(&database.Object{Data: resourceTypeResource}, nil).Times(1) databaseClient.EXPECT(). Get(gomock.Any(), id.RootScope(), gomock.Any()). Return(&database.Object{Data: resourceGroup}, nil).Times(1) + databaseClient.EXPECT(). + Get(gomock.Any(), locationResource.ID). + Return(&database.Object{Data: locationResource}, nil).Times(1) + // Tracking entry created existingEntry := &database.Object{ Data: &datamodel.GenericResource{ diff --git a/pkg/ucp/frontend/controller/resourcegroups/util.go b/pkg/ucp/frontend/controller/resourcegroups/util.go index 6c9ab112a3..5d41849821 100644 --- a/pkg/ucp/frontend/controller/resourcegroups/util.go +++ b/pkg/ucp/frontend/controller/resourcegroups/util.go @@ -123,12 +123,7 @@ func ValidateResourceType(ctx context.Context, client database.Client, id resour _, err = database.GetResource[datamodel.ResourceType](ctx, client, resourceTypeID.String()) if errors.Is(err, &database.ErrNotFound{}) { - - // Return the error as-is to fallback to the legacy routing behavior. - return nil, err - - // Uncomment this when we remove the legacy routing behavior. - // return nil, &InvalidError{Message: fmt.Sprintf("resource type %q not found", id.Type())} + return nil, &InvalidError{Message: fmt.Sprintf("resource type %q not found", id.Type())} } else if err != nil { return nil, fmt.Errorf("failed to fetch resource type %q: %w", id.Type(), err) } @@ -141,12 +136,7 @@ func ValidateResourceType(ctx context.Context, client database.Client, id resour location, err := database.GetResource[datamodel.Location](ctx, client, locationID.String()) if errors.Is(err, &database.ErrNotFound{}) { - - // Return the error as-is to fallback to the legacy routing behavior. - return nil, err - - // Uncomment this when we remove the legacy routing behavior. - // return nil, &InvalidError{Message: fmt.Sprintf("location %q not found for resource provider %q", locationName, id.ProviderNamespace())} + return nil, &InvalidError{Message: fmt.Sprintf("location %q not found for resource provider %q", locationName, id.ProviderNamespace())} } else if err != nil { return nil, fmt.Errorf("failed to fetch location %q: %w", locationID.String(), err) } @@ -230,22 +220,6 @@ func isOperationResourceType(id resources.ID) bool { return false } -// ValidateLegacyResourceProvider validates that the resource provider specified in the id exists. Returns InvalidError if the plane -// contains invalid data. -func ValidateLegacyResourceProvider(ctx context.Context, client database.Client, id resources.ID, plane *datamodel.RadiusPlane) (*url.URL, error) { - downstream := plane.LookupResourceProvider(id.ProviderNamespace()) - if downstream == "" { - return nil, &InvalidError{Message: fmt.Sprintf("resource provider %s not configured", id.ProviderNamespace())} - } - - downstreamURL, err := url.Parse(downstream) - if err != nil { - return nil, &InvalidError{Message: fmt.Sprintf("failed to parse downstream URL: %v", err.Error())} - } - - return downstreamURL, nil -} - // ValidateDownstream can be used to find and validate the downstream URL for a resource. // Returns NotFoundError for the case where the plane or resource group does not exist. // Returns InvalidError for cases where the data is invalid, like when the resource provider is not configured. @@ -255,12 +229,11 @@ func ValidateDownstream(ctx context.Context, client database.Client, id resource // - The plane exists // - The resource group exists // - The resource provider is configured .. either: - // - As part of the plane (legacy routing) // - As part of a resource provider resource (System.Resources/resourceProviders) (new/UDT routing) // // The plane exists. - plane, err := ValidateRadiusPlane(ctx, client, id) + _, err := ValidateRadiusPlane(ctx, client, id) if err != nil { return nil, err } @@ -273,10 +246,7 @@ func ValidateDownstream(ctx context.Context, client database.Client, id resource // If this returns success, it means the resource type is configured using new/UDT routing. downstreamURL, err := ValidateResourceType(ctx, client, id, location, apiVersion) - if errors.Is(err, &database.ErrNotFound{}) { - // If the resource provider is not found, treat it like a legacy provider. - return ValidateLegacyResourceProvider(ctx, client, id, plane) - } else if err != nil { + if err != nil { return nil, err } diff --git a/pkg/ucp/frontend/controller/resourcegroups/util_test.go b/pkg/ucp/frontend/controller/resourcegroups/util_test.go index 3b80d21bc3..085f1de9b2 100644 --- a/pkg/ucp/frontend/controller/resourcegroups/util_test.go +++ b/pkg/ucp/frontend/controller/resourcegroups/util_test.go @@ -409,211 +409,3 @@ func Test_ValidateDownstream(t *testing.T) { require.Nil(t, downstreamURL) }) } - -// This test validates the pre-UDT before where resource providers are registered as part of the plane. -// This can be deleted once the legacy routing behavior is removed. -func Test_ValidateDownstream_Legacy(t *testing.T) { - id, err := resources.ParseResource("/planes/radius/local/resourceGroups/test-group/providers/System.TestRP/testResources/name") - require.NoError(t, err) - - idWithoutResourceGroup, err := resources.Parse("/planes/radius/local/providers/System.TestRP/testResources") - require.NoError(t, err) - - resourceTypeID, err := datamodel.ResourceTypeIDFromResourceID(id) - require.NoError(t, err) - - downstream := "http://localhost:7443" - - plane := &datamodel.RadiusPlane{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.PlaneScope(), - }, - }, - Properties: datamodel.RadiusPlaneProperties{ - ResourceProviders: map[string]string{ - "System.TestRP": downstream, - }, - }, - } - - setup := func(t *testing.T) *database.MockClient { - ctrl := gomock.NewController(t) - return database.NewMockClient(ctrl) - } - - t.Run("success (resource group)", func(t *testing.T) { - resourceGroup := &datamodel.ResourceGroup{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.RootScope(), - }, - }, - } - - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(&database.Object{Data: resourceGroup}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), resourceTypeID.String()).Return(nil, &database.ErrNotFound{}).Times(1) - - expectedURL, err := url.Parse(downstream) - require.NoError(t, err) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.NoError(t, err) - require.Equal(t, expectedURL, downstreamURL) - }) - - t.Run("success (non resource group)", func(t *testing.T) { - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), idWithoutResourceGroup.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), resourceTypeID.String()).Return(nil, &database.ErrNotFound{}).Times(1) - - expectedURL, err := url.Parse(downstream) - require.NoError(t, err) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, idWithoutResourceGroup, location, apiVersion) - require.NoError(t, err) - require.Equal(t, expectedURL, downstreamURL) - }) - - t.Run("plane not found", func(t *testing.T) { - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(nil, &database.ErrNotFound{}).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, &NotFoundError{Message: "plane \"/planes/radius/local\" not found"}, err) - require.Nil(t, downstreamURL) - }) - - t.Run("plane retrieval failure", func(t *testing.T) { - databaseClient := setup(t) - - expected := fmt.Errorf("failed to fetch plane \"/planes/radius/local\": %w", errors.New("test error")) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(nil, errors.New("test error")).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, expected, err) - require.Nil(t, downstreamURL) - }) - - t.Run("resource group not found", func(t *testing.T) { - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(nil, &database.ErrNotFound{}).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, &NotFoundError{Message: "resource group \"/planes/radius/local/resourceGroups/test-group\" not found"}, err) - require.Nil(t, downstreamURL) - }) - - t.Run("resource group err", func(t *testing.T) { - databaseClient := setup(t) - - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(nil, errors.New("test error")).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, "failed to fetch resource group \"/planes/radius/local/resourceGroups/test-group\": test error", err.Error()) - require.Nil(t, downstreamURL) - }) - - t.Run("legacy resource provider not configured", func(t *testing.T) { - plane := &datamodel.RadiusPlane{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.PlaneScope(), - }, - }, - Properties: datamodel.RadiusPlaneProperties{ - ResourceProviders: map[string]string{}, - }, - } - - resourceGroup := &datamodel.ResourceGroup{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.RootScope(), - }, - }, - } - - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(&database.Object{Data: resourceGroup}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), resourceTypeID.String()).Return(nil, &database.ErrNotFound{}).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, &InvalidError{Message: "resource provider System.TestRP not configured"}, err) - require.Nil(t, downstreamURL) - }) - - t.Run("location not found", func(t *testing.T) { - plane := &datamodel.RadiusPlane{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.PlaneScope(), - }, - }, - Properties: datamodel.RadiusPlaneProperties{ - ResourceProviders: map[string]string{}, - }, - } - - resourceGroup := &datamodel.ResourceGroup{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.RootScope(), - }, - }, - } - - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(&database.Object{Data: resourceGroup}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), resourceTypeID.String()).Return(nil, &database.ErrNotFound{}).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, &InvalidError{Message: "resource provider System.TestRP not configured"}, err) - require.Nil(t, downstreamURL) - }) - - t.Run("resource provider invalid URL", func(t *testing.T) { - plane := &datamodel.RadiusPlane{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.PlaneScope(), - }, - }, - Properties: datamodel.RadiusPlaneProperties{ - ResourceProviders: map[string]string{ - "System.TestRP": "\ninvalid", - }, - }, - } - - resourceGroup := &datamodel.ResourceGroup{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: id.RootScope(), - }, - }, - } - - databaseClient := setup(t) - databaseClient.EXPECT().Get(gomock.Any(), id.PlaneScope()).Return(&database.Object{Data: plane}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), id.RootScope()).Return(&database.Object{Data: resourceGroup}, nil).Times(1) - databaseClient.EXPECT().Get(gomock.Any(), resourceTypeID.String()).Return(nil, &database.ErrNotFound{}).Times(1) - - downstreamURL, err := ValidateDownstream(testcontext.New(t), databaseClient, id, location, apiVersion) - require.Error(t, err) - require.Equal(t, &InvalidError{Message: "failed to parse downstream URL: parse \"\\ninvalid\": net/url: invalid control character in URL"}, err) - require.Nil(t, downstreamURL) - }) -} diff --git a/pkg/ucp/integrationtests/radius/proxy_test.go b/pkg/ucp/integrationtests/radius/proxy_test.go index 5275034f31..5c93754d61 100644 --- a/pkg/ucp/integrationtests/radius/proxy_test.go +++ b/pkg/ucp/integrationtests/radius/proxy_test.go @@ -39,8 +39,11 @@ const ( testRadiusPlaneID = "/planes/radius/test" testResourceNamespace = "System.Test" testResourceGroupID = testRadiusPlaneID + "/resourceGroups/test-rg" + testResourceProviderID = testRadiusPlaneID + "/providers/System.Resources/resourceproviders/System.Test" testResourceCollectionID = testResourceGroupID + "/providers/System.Test/testResources" testResourceID = testResourceCollectionID + "/test-resource" + resourceTypeURL = testResourceProviderID + "/resourcetypes/testResources" + locationID = testResourceProviderID + "/locations/global" assertTimeout = time.Second * 10 assertRetry = time.Second * 2 @@ -64,14 +67,20 @@ func Test_RadiusPlane_ResourceSync(t *testing.T) { ucp := testhost.Start(t) rp := testrp.Start(t) rp.Handler = testrp.SyncResource(t, ucp, testResourceGroupID) - + address := to.Ptr("http://" + rp.Address()) rps := map[string]*string{ - testResourceNamespace: to.Ptr("http://" + rp.Address()), + testResourceNamespace: address, } createRadiusPlane(ucp, rps) createResourceGroup(ucp, testResourceGroupID) + createResourceProvider(ucp) + + createResourceType(ucp, resourceTypeURL) + + createLocation(ucp, address) + message := "here is some test data" expectedTrackedResource := v20231001preview.GenericResource{ @@ -89,7 +98,10 @@ func Test_RadiusPlane_ResourceSync(t *testing.T) { body, err := json.Marshal(data) require.NoError(t, err) - response := ucp.MakeRequest(http.MethodPut, testResourceID+"?api-version="+testrp.Version, body) + response := ucp.MakeRequest(http.MethodGet, resourceTypeURL+"?api-version="+testrp.Version, nil) + response.EqualsStatusCode(http.StatusOK) + + response = ucp.MakeRequest(http.MethodPut, testResourceID+"?api-version="+testrp.Version, body) response.EqualsStatusCode(http.StatusOK) resource := &testrp.TestResource{} @@ -184,14 +196,21 @@ func Test_RadiusPlane_ResourceAsync(t *testing.T) { } rp.Handler = testrp.AsyncResource(t, ucp, testResourceGroupID, onPut, onDelete) - + address := to.Ptr("http://" + rp.Address()) rps := map[string]*string{ - testResourceNamespace: to.Ptr("http://" + rp.Address()), + testResourceNamespace: address, } + createRadiusPlane(ucp, rps) createResourceGroup(ucp, testResourceGroupID) + createResourceProvider(ucp) + + createResourceType(ucp, resourceTypeURL) + + createLocation(ucp, address) + message := "here is some test data" expectedTrackedResource := v20231001preview.GenericResource{ @@ -400,3 +419,44 @@ func createResourceGroup(ucp *testhost.TestHost, id string) { response := ucp.MakeTypedRequest(http.MethodPut, id+"?"+apiVersionParameter, body) response.EqualsStatusCode(http.StatusOK) } + +func createResourceProvider(ucp *testhost.TestHost) { + body := v20231001preview.ResourceProviderResource{ + Location: to.Ptr(v1.LocationGlobal), + Properties: &v20231001preview.ResourceProviderProperties{}, + } + response := ucp.MakeTypedRequest("PUT", testResourceProviderID+"?"+apiVersionParameter, body) + response.WaitForOperationComplete(nil) + response.EqualsStatusCode(http.StatusCreated) +} + +func createResourceType(ucp *testhost.TestHost, id string) { + body := v20231001preview.ResourceTypeResource{ + Properties: &v20231001preview.ResourceTypeProperties{ + DefaultAPIVersion: to.Ptr("2023-10-01-preview"), + }, + } + + response := ucp.MakeTypedRequest(http.MethodPut, id+"?"+apiVersionParameter, body) + response.WaitForOperationComplete(nil) + response.EqualsStatusCode(http.StatusCreated) +} + +func createLocation(server *testhost.TestHost, address *string) { + body := v20231001preview.LocationResource{ + Properties: &v20231001preview.LocationProperties{ + Address: address, + ResourceTypes: map[string]*v20231001preview.LocationResourceType{ + "testResources": { + APIVersions: map[string]map[string]any{ + "2023-10-01-preview": {}, + }, + }, + }, + }, + } + + response := server.MakeTypedRequest("PUT", locationID+"?"+apiVersionParameter, body) + response.WaitForOperationComplete(nil) + response.EqualsStatusCode(http.StatusCreated) +} diff --git a/pkg/ucp/integrationtests/resourceproviders/testdata/manifests/resourceprovider-valid1.yaml b/pkg/ucp/integrationtests/resourceproviders/testdata/manifests/resourceprovider-valid1.yaml index 4523f045df..02118b74c2 100644 --- a/pkg/ucp/integrationtests/resourceproviders/testdata/manifests/resourceprovider-valid1.yaml +++ b/pkg/ucp/integrationtests/resourceproviders/testdata/manifests/resourceprovider-valid1.yaml @@ -1,4 +1,7 @@ name: TestProvider.TestCompany +location: + global: + 'http://localhost:8080' types: testResourcesAbc: apiVersions: diff --git a/test/functional-portable/ucp/noncloud/resourceprovider_test.go b/test/functional-portable/ucp/noncloud/resourceprovider_test.go index ba70e34263..95f46fb63b 100644 --- a/test/functional-portable/ucp/noncloud/resourceprovider_test.go +++ b/test/functional-portable/ucp/noncloud/resourceprovider_test.go @@ -38,7 +38,7 @@ func Test_ResourceProviderRegistration(t *testing.T) { manifestPath = "testdata/resourceprovider.yaml" resourceProviderName = "MyCompany.Resources" expectedResourceTypeName = "testResources" - expectedApiVersion = "2025-01-01-preview" + expectedApiVersion = "2023-10-01-preview" ) expectedData := map[string]any{ diff --git a/test/functional-portable/ucp/noncloud/testdata/resourceprovider.yaml b/test/functional-portable/ucp/noncloud/testdata/resourceprovider.yaml index c3348b0229..84dbc04431 100644 --- a/test/functional-portable/ucp/noncloud/testdata/resourceprovider.yaml +++ b/test/functional-portable/ucp/noncloud/testdata/resourceprovider.yaml @@ -2,6 +2,6 @@ name: MyCompany.Resources types: testResources: apiVersions: - "2025-01-01-preview": + "2023-10-01-preview": schema: {} capabilities: ["SupportsRecipes"]