From 9b876858b465222b126a59e44b9f37f54b5aaed9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 12 Dec 2024 09:41:01 -0800 Subject: [PATCH] Remove CosmosDB database provider (#8116) # Description This is part of a refactor of the data layer to simplify the design. The CosmosDB implementation is complicated and unused so we're removing it. ## 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). ## Contributor checklist Please verify that the PR meets the following requirements, where applicable: - [ ] An overview of proposed schema changes is included in a linked GitHub issue. - [ ] A design document PR is created in the [design-notes repository](https://github.com/radius-project/design-notes/), if new APIs are being introduced. - [ ] If applicable, design document has been reviewed and approved by Radius maintainers/approvers. - [ ] A PR for the [samples repository](https://github.com/radius-project/samples) is created, if existing samples are affected by the changes in this PR. - [ ] A PR for the [documentation repository](https://github.com/radius-project/docs) is created, if the changes in this PR affect the documentation or any user facing updates are made. - [ ] A PR for the [recipes repository](https://github.com/radius-project/recipes) is created, if existing recipes are affected by the changes in this PR. Signed-off-by: Ryan Nowak Co-authored-by: Yetkin Timocin --- cmd/applications-rp/radius-cloud.yaml | 56 -- .../configSettings.md | 11 +- .../first-commit-05-running-tests/index.md | 1 - go.mod | 2 - go.sum | 7 - pkg/armrpc/hostoptions/hostoptions.go | 10 - pkg/corerp/README.md | 9 +- pkg/ucp/dataprovider/factory.go | 22 - pkg/ucp/dataprovider/types.go | 3 - .../store/cosmosdb/cosmosdbstorageclient.go | 460 ---------- .../cosmosdb/cosmosdbstorageclient_test.go | 786 ------------------ pkg/ucp/store/cosmosdb/options.go | 52 -- pkg/ucp/store/cosmosdb/util.go | 154 ---- pkg/ucp/store/cosmosdb/util_test.go | 199 ----- 14 files changed, 4 insertions(+), 1768 deletions(-) delete mode 100644 cmd/applications-rp/radius-cloud.yaml delete mode 100644 pkg/ucp/store/cosmosdb/cosmosdbstorageclient.go delete mode 100644 pkg/ucp/store/cosmosdb/cosmosdbstorageclient_test.go delete mode 100644 pkg/ucp/store/cosmosdb/options.go delete mode 100644 pkg/ucp/store/cosmosdb/util.go delete mode 100644 pkg/ucp/store/cosmosdb/util_test.go diff --git a/cmd/applications-rp/radius-cloud.yaml b/cmd/applications-rp/radius-cloud.yaml deleted file mode 100644 index acbcbe2401..0000000000 --- a/cmd/applications-rp/radius-cloud.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# This is an example of configuration file. -environment: - name: AzureCloud - roleLocation: West US -identity: # 1P AAD APP authentication - clientId: "PLACEHOLDER" - instance: "https://login.windows.net" - tenantId: "common" - armEndpoint: "https://management.azure.com:443" - audience: "https://management.core.windows.net" - pemCertPath: "/var/certs/rp-aad-app.pem" -storageProvider: - provider: "cosmosdb" - cosmosdb: - # Create your own SQL API Cosmos DB account and set url in this configuration or to RADIUS_STORAGEPROVIDER_COSMOSDB_URL environment variable - url: https://radius-eastus-test.documents.azure.com:443/ - database: applicationscore - # Set primary key to in this configuration or to RADIUS_STORAGEPROVIDER_COSMOSDB_MASTERKEY environment variable - masterKey: set-me-in-a-different-way -queueProvider: - provider: inmemory - name: radius -profilerProvider: - enabled: true - port: 6060 -secretProvider: - provider: etcd - etcd: - inmemory: true -server: - host: "0.0.0.0" - port: 8080 - authType: "ClientCertificate" - enableArmAuth: true - armMetadataEndpoint: "https://admin.api-dogfood.resources.windows-int.net/metadata/authentication?api-version=2015-01-01" -workerServer: - maxOperationConcurrency: 10 - maxOperationRetryCount: 2 -metricsProvider: - prometheus: - enabled: true - path: "/metrics" - port: 9090 -featureFlags: - - "PLACEHOLDER" -ucp: - kind: kubernetes - # Logging configuration -logging: - level: "info" - json: false -bicep: - deleteRetryCount: 20 - deleteRetryDelaySeconds: 60 -terraform: - path: "/terraform" 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 c350891370..e54edf127a 100644 --- a/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md +++ b/docs/contributing/contributing-code/contributing-code-control-plane/configSettings.md @@ -65,8 +65,7 @@ The following are properties that can be specified for UCP: | Key | Description | Example | |-----|-------------|---------| | provider | The type of storage provider | `apiServer` | -| apiServer | Object containing properties for Kubernetes APIServer store | [**See below**](#apiserver) | -| cosmosdb | Object containing properties for CosmosDB | [**See below**](#cosmosdb) | +| apiServer | Object containing properties for Kubernetes APIServer store | [**See below**](#apiserver) | | etcd | Object containing properties for ETCD store | [**See below**](#etcd)| ### queueProvider @@ -159,14 +158,6 @@ ucp: |-----|-------------|---------| | inMemory | Configures the etcd store to run in-memory with the resource provider (must be `true`/`false`) | `true` | -### cosmosdb -| Key | Description | Example | -|-----|-------------|---------| -| url | URL of CosmosDB account | `https://radius-eastus-test.documents.azure.com:443/` | -| database | Name of the database in account | `applicationscore` | -| masterKey | All access key token for database resources | `your-master-key` | -| CollectionThroughput | Throughput of database | `400` | - ## Plane properties | Key | Description | Example | diff --git a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-05-running-tests/index.md b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-05-running-tests/index.md index 18f9532339..f55098ea20 100644 --- a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-05-running-tests/index.md +++ b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-05-running-tests/index.md @@ -24,7 +24,6 @@ ok github.com/radius-project/radius/pkg/cli 0.250s ? github.com/radius-project/radius/pkg/azure/radclient [no test files] ? github.com/radius-project/radius/pkg/renderers [no test files] ok github.com/radius-project/radius/pkg/renderers/containerv1alpha3 -ok github.com/radius-project/radius/pkg/renderers/cosmosdbmongov1alpha3 ok github.com/radius-project/radius/pkg/renderers/dapr ok github.com/radius-project/radius/pkg/renderers/daprpubsubv1alpha3 ok github.com/radius-project/radius/pkg/renderers/daprstatestorev1alpha3 diff --git a/go.mod b/go.mod index ea24574e14..73e3159556 100644 --- a/go.mod +++ b/go.mod @@ -61,13 +61,11 @@ require ( github.com/opencontainers/image-spec v1.1.0 github.com/projectcontour/contour v1.30.1 github.com/prometheus/client_golang v1.20.5 - github.com/spaolacci/murmur3 v1.1.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stern/stern v1.31.0 github.com/stretchr/testify v1.10.0 - github.com/vippsas/go-cosmosdb v0.0.0-20230118095602-f4e4b9f1c352 github.com/wI2L/jsondiff v0.6.1 go.etcd.io/etcd/client/v3 v3.5.17 go.etcd.io/etcd/server/v3 v3.5.17 diff --git a/go.sum b/go.sum index 205064fcc5..5004b94645 100644 --- a/go.sum +++ b/go.sum @@ -265,7 +265,6 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -549,7 +548,6 @@ github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= -github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -759,7 +757,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= @@ -947,8 +944,6 @@ github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= @@ -997,8 +992,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/vippsas/go-cosmosdb v0.0.0-20230118095602-f4e4b9f1c352 h1:N4BwZihfs9zOPo1R+aSIyLmM2qRYoN2gEuleOHUhjBA= -github.com/vippsas/go-cosmosdb v0.0.0-20230118095602-f4e4b9f1c352/go.mod h1:MC5grluKkU7tz2VMDXi7AOj2N4spGFebM5w//g2WpyU= github.com/wI2L/jsondiff v0.6.1 h1:ISZb9oNWbP64LHnu4AUhsMF5W0FIj5Ok3Krip9Shqpw= github.com/wI2L/jsondiff v0.6.1/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/pkg/armrpc/hostoptions/hostoptions.go b/pkg/armrpc/hostoptions/hostoptions.go index 83aaea0922..f669c93f92 100644 --- a/pkg/armrpc/hostoptions/hostoptions.go +++ b/pkg/armrpc/hostoptions/hostoptions.go @@ -124,16 +124,6 @@ func loadConfig(configPath string) (*ProviderConfig, error) { return nil, fmt.Errorf("failed to load yaml: %w", err) } - cosmosdbUrl := os.Getenv("RADIUS_STORAGEPROVIDER_COSMOSDB_URL") - if cosmosdbUrl != "" { - conf.StorageProvider.CosmosDB.Url = cosmosdbUrl - } - - cosmosDBKey := os.Getenv("RADIUS_STORAGEPROVIDER_COSMOSDB_MASTERKEY") - if cosmosDBKey != "" { - conf.StorageProvider.CosmosDB.MasterKey = cosmosDBKey - } - return conf, nil } diff --git a/pkg/corerp/README.md b/pkg/corerp/README.md index cd0e516e77..76cbc8ad2b 100644 --- a/pkg/corerp/README.md +++ b/pkg/corerp/README.md @@ -24,10 +24,9 @@ ## How to Run and Test Core RP -1. Update StorageProvider section of `cmd/applications-rp/radius-dev.yaml` by adding your Cosmos DB URL and key 1. With `cmd/applications-rp/main.go` file open, go to `Run And Debug` view in VS Code and click `Run` -1. You should have the service up and running at `localhost:8080` now -1. To create or update an environment, here is an example curl command: +2. You should have the service up and running at `localhost:8080` now +3. To create or update an environment, here is an example curl command: ``` curl --location --request PUT 'http://localhost:8080/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/env0?api-version=2023-10-01-preview' \ @@ -43,14 +42,12 @@ }' ``` -1. To get information about an environment, here is an example curl command: +4. To get information about an environment, here is an example curl command: ``` curl --location --request GET 'http://localhost:8080/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/?api-version=2023-10-01-preview' ``` -1. You should also be able to see all changes in Cosmos DB - ## References * [ARM RPC v1.0 Specification](https://github.com/Azure/azure-resource-manager-rpc) diff --git a/pkg/ucp/dataprovider/factory.go b/pkg/ucp/dataprovider/factory.go index 14696633fc..1d0580336a 100644 --- a/pkg/ucp/dataprovider/factory.go +++ b/pkg/ucp/dataprovider/factory.go @@ -27,7 +27,6 @@ import ( store "github.com/radius-project/radius/pkg/ucp/store" "github.com/radius-project/radius/pkg/ucp/store/apiserverstore" ucpv1alpha1 "github.com/radius-project/radius/pkg/ucp/store/apiserverstore/api/ucp.dev/v1alpha1" - "github.com/radius-project/radius/pkg/ucp/store/cosmosdb" "github.com/radius-project/radius/pkg/ucp/store/etcdstore" "github.com/radius-project/radius/pkg/ucp/store/inmemory" "github.com/radius-project/radius/pkg/ucp/store/postgres" @@ -41,7 +40,6 @@ type storageFactoryFunc func(context.Context, StorageProviderOptions, string) (s var storageClientFactory = map[StorageProviderType]storageFactoryFunc{ TypeAPIServer: initAPIServerClient, - TypeCosmosDB: initCosmosDBClient, TypeETCD: InitETCDClient, TypeInMemory: initInMemoryClient, TypePostgreSQL: initPostgreSQLClient, @@ -83,26 +81,6 @@ func initAPIServerClient(ctx context.Context, opt StorageProviderOptions, _ stri return client, nil } -func initCosmosDBClient(ctx context.Context, opt StorageProviderOptions, collectionName string) (store.StorageClient, error) { - sopt := &cosmosdb.ConnectionOptions{ - Url: opt.CosmosDB.Url, - DatabaseName: opt.CosmosDB.Database, - CollectionName: collectionName, - MasterKey: opt.CosmosDB.MasterKey, - CollectionThroughput: opt.CosmosDB.CollectionThroughput, - } - dbclient, err := cosmosdb.NewCosmosDBStorageClient(sopt) - if err != nil { - return nil, fmt.Errorf("failed to create CosmosDB client - configuration may be invalid: %w", err) - } - - if err = dbclient.Init(ctx); err != nil { - return nil, fmt.Errorf("failed to initialize CosmosDB client - configuration may be invalid: %w", err) - } - - return dbclient, nil -} - // InitETCDClient checks if the ETCD client is in memory and if the client is not nil, then it initializes the storage // client and returns an ETCDClient. If either of these conditions are not met, an error is returned. func InitETCDClient(ctx context.Context, opt StorageProviderOptions, _ string) (store.StorageClient, error) { diff --git a/pkg/ucp/dataprovider/types.go b/pkg/ucp/dataprovider/types.go index 543ef8dfd8..b6f9e8ce9b 100644 --- a/pkg/ucp/dataprovider/types.go +++ b/pkg/ucp/dataprovider/types.go @@ -29,9 +29,6 @@ const ( // TypeAPIServer represents the Kubernetes APIServer provider. TypeAPIServer StorageProviderType = "apiserver" - // TypeCosmosDB represents CosmosDB provider. - TypeCosmosDB StorageProviderType = "cosmosdb" - // TypeETCD represents the etcd provider. TypeETCD StorageProviderType = "etcd" diff --git a/pkg/ucp/store/cosmosdb/cosmosdbstorageclient.go b/pkg/ucp/store/cosmosdb/cosmosdbstorageclient.go deleted file mode 100644 index 0291235fa0..0000000000 --- a/pkg/ucp/store/cosmosdb/cosmosdbstorageclient.go +++ /dev/null @@ -1,460 +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 cosmosdb - -import ( - "context" - "fmt" - "strings" - - "github.com/radius-project/radius/pkg/ucp/resources" - resources_azure "github.com/radius-project/radius/pkg/ucp/resources/azure" - "github.com/radius-project/radius/pkg/ucp/store" - "github.com/vippsas/go-cosmosdb/cosmosapi" -) - -const ( - // PartitionKeyName is the property used for partitioning. - PartitionKeyName = "/partitionKey" - - // go-cosmosdb does not return the error response code. Comparing error message is the only way to check the errors. - // Once we move to official Go SDK, we can have the better error handling. - // TODO: Switch to the official cosmosdb SDK - https://github.com/radius-project/radius/issues/2225 - // 1. Repalce github.com/vippsas/go-cosmosdb/cosmosapi with the official sdk when it supports query api. - // 2. Improve error handling using response code instead of string match. - errResourceNotFoundMsg = "Resource that no longer exists" - errIDConflictMsg = "The ID provided has been taken by an existing resource" - errEtagPreconditionMsgPrefix = "The operation specified an eTag" -) - -var _ store.StorageClient = (*CosmosDBStorageClient)(nil) - -// ResourceEntity represents the default envelope model to store resource metadata. -type ResourceEntity struct { - // CosmosDB system-related properties. - // ID represents the primary key. - ID string `json:"id"` - // ETag represents an etag required for optimistic concurrency control. - ETag string `json:"_etag"` - // Self represents the unique addressable URI for the resource. - Self string `json:"_self"` - // Timestamp represents the last updated timestamp of the resource. - UpdatedTime int `json:"_ts"` - - // ResourceID represents fully qualified resource id. - ResourceID string `json:"resourceId"` - // RootScope represents root scope such as subscription id. - RootScope string `json:"rootScope"` - // ResourceGroup represents fully qualified resource scope. - ResourceGroup string `json:"resourceGroup"` - // PartitionKey represents the key used for partitioning. - PartitionKey string `json:"partitionKey"` - // Entity represents the resource metadata. - Entity any `json:"entity"` -} - -// CosmosDBStorageClient implements CosmosDB stroage client. -type CosmosDBStorageClient struct { - client *cosmosapi.Client - options *ConnectionOptions -} - -// NewCosmosDBStorageClient creates a new CosmosDBStorageClient instance using the provided ConnectionOptions and returns -// it, or an error if the ConnectionOptions are invalid. -func NewCosmosDBStorageClient(options *ConnectionOptions) (*CosmosDBStorageClient, error) { - if err := options.load(); err != nil { - return nil, err - } - - cfg := cosmosapi.Config{ - MasterKey: options.MasterKey, - MaxRetries: 5, - } - - client := cosmosapi.New(options.Url, cfg, nil, nil) - - return &CosmosDBStorageClient{ - client: client, - options: options, - }, nil -} - -// Init checks if the database and collection exist, and if not, creates them. It returns an error if -// either of the checks or creations fail. -func (c *CosmosDBStorageClient) Init(ctx context.Context) error { - if err := c.createDatabaseIfNotExists(ctx); err != nil { - return err - } - if err := c.createCollectionIfNotExists(ctx); err != nil { - return err - } - return nil -} - -func (c *CosmosDBStorageClient) createDatabaseIfNotExists(ctx context.Context) error { - _, err := c.client.GetDatabase(ctx, c.options.DatabaseName, nil) - if err == nil { - return nil - } - if !strings.EqualFold(err.Error(), errResourceNotFoundMsg) { - return err - } - - _, err = c.client.CreateDatabase(ctx, c.options.DatabaseName, nil) - if err != nil && strings.EqualFold(err.Error(), errIDConflictMsg) { - return nil - } - - return err -} - -func (c *CosmosDBStorageClient) createCollectionIfNotExists(ctx context.Context) error { - _, err := c.client.GetCollection(ctx, c.options.DatabaseName, c.options.CollectionName) - if err == nil { - return nil - } - if !strings.EqualFold(err.Error(), errResourceNotFoundMsg) { - return err - } - opt := cosmosapi.CreateCollectionOptions{ - Id: c.options.CollectionName, - IndexingPolicy: &cosmosapi.IndexingPolicy{ - IndexingMode: cosmosapi.IndexingMode("consistent"), - Automatic: true, - Included: []cosmosapi.IncludedPath{ - { - Path: "/*", - Indexes: []cosmosapi.Index{ - { - Kind: cosmosapi.Range, - DataType: cosmosapi.StringType, - Precision: -1, - }, - { - Kind: cosmosapi.Range, - DataType: cosmosapi.NumberType, - Precision: -1, - }, - }, - }, - }, - }, - PartitionKey: &cosmosapi.PartitionKey{ - Paths: []string{ - PartitionKeyName, - }, - Kind: "Hash", - }, - } - - // CollectionThroughput needs to be set only if radius uses Provioned throughput mode. - if c.options.CollectionThroughput > 0 { - opt.OfferThroughput = cosmosapi.OfferThroughput(c.options.CollectionThroughput) - } - - _, err = c.client.CreateCollection(context.Background(), c.options.DatabaseName, opt) - - if err != nil && strings.EqualFold(err.Error(), errIDConflictMsg) { - return nil - } - - return err -} - -func constructCosmosDBQuery(query store.Query) (*cosmosapi.Query, error) { - if query.RoutingScopePrefix != "" { - return nil, &store.ErrInvalid{Message: "RoutingScopePrefix is not supported."} - } - - if query.RootScope == "" { - return nil, &store.ErrInvalid{Message: "RootScope can not be empty."} - } - - queryString := "SELECT * FROM c WHERE " - whereParam := "" - queryParams := []cosmosapi.QueryParam{} - - if query.ScopeRecursive { - whereParam = whereParam + "STARTSWITH(c.rootScope, @rootScope, true)" - queryParams = append(queryParams, cosmosapi.QueryParam{ - Name: "@rootScope", - Value: strings.ToLower(query.RootScope), - }) - } else { - whereParam = whereParam + "c.rootScope = @rootScope" - queryParams = append(queryParams, cosmosapi.QueryParam{ - Name: "@rootScope", - Value: strings.ToLower(query.RootScope), - }) - } - - if query.ResourceType != "" { - if whereParam != "" { - whereParam += " and " - } - whereParam += "STRINGEQUALS(c.entity.type, @rtype, true)" - queryParams = append(queryParams, cosmosapi.QueryParam{ - Name: "@rtype", - Value: query.ResourceType, - }) - } - - for i, filter := range query.Filters { - if whereParam != "" { - whereParam += " and " - } - filterParam := fmt.Sprintf("filter%d", i) - whereParam += fmt.Sprintf("STRINGEQUALS(c.entity.%s, @%s, true)", filter.Field, filterParam) - queryParams = append(queryParams, cosmosapi.QueryParam{ - Name: "@" + filterParam, - Value: filter.Value, - }) - } - - if whereParam == "" { - return nil, &store.ErrInvalid{Message: "invalid Query parameters"} - } - - return &cosmosapi.Query{Query: queryString + whereParam, Params: queryParams}, nil -} - -// Query builds and executes a CosmosDB query based on the provided store.Query and returns the results. -func (c *CosmosDBStorageClient) Query(ctx context.Context, query store.Query, opts ...store.QueryOptions) (*store.ObjectQueryResult, error) { - if ctx == nil { - return nil, &store.ErrInvalid{Message: "invalid argument. 'ctx' is required"} - } - err := query.Validate() - if err != nil { - return nil, &store.ErrInvalid{Message: fmt.Sprintf("invalid argument. Query is invalid: %s", err.Error())} - } - - cfg := store.NewQueryConfig(opts...) - - resourceID, err := resources.ParseScope(query.RootScope) - if err != nil { - return nil, err - } - - qry, err := constructCosmosDBQuery(query) - if err != nil { - return nil, err - } - - entities := []ResourceEntity{} - - maxItemCount := c.options.DefaultQueryItemCount - if cfg.MaxQueryItemCount > 0 { - maxItemCount = cfg.MaxQueryItemCount - } - - qops := cosmosapi.QueryDocumentsOptions{ - IsQuery: true, - ContentType: cosmosapi.QUERY_CONTENT_TYPE, - MaxItemCount: maxItemCount, - EnableCrossPartition: true, - ConsistencyLevel: cosmosapi.ConsistencyLevelEventual, - } - - partitionKey, err := GetPartitionKey(resourceID) - if err != nil { - return nil, err - } - - if partitionKey != "" { - qops.PartitionKeyValue = partitionKey - qops.EnableCrossPartition = false - } - - if cfg.PaginationToken != "" { - qops.Continuation = cfg.PaginationToken - } - - resp, err := c.client.QueryDocuments(ctx, c.options.DatabaseName, c.options.CollectionName, *qry, &entities, qops) - if err != nil { - return nil, err - } - - output := []store.Object{} - for _, entity := range entities { - output = append(output, store.Object{ - Metadata: store.Metadata{ - ID: entity.ResourceID, - ETag: entity.ETag, - }, - Data: entity.Entity, - }) - } - - return &store.ObjectQueryResult{ - PaginationToken: resp.Continuation, - Items: output, - }, nil -} - -// Get retrieves an object using CosmosDBStorageClient using the provided ID and optional GetOptions. It returns an error -// if the object is not found or if an error occurs while retrieving the object. -func (c *CosmosDBStorageClient) Get(ctx context.Context, id string, opts ...store.GetOptions) (*store.Object, error) { - parsedID, err := resources.Parse(id) - if err != nil { - return nil, err - } - - partitionKey, err := GetPartitionKey(parsedID) - if err != nil { - return nil, err - } - - ops := cosmosapi.GetDocumentOptions{ - PartitionKeyValue: partitionKey, - } - - docID, err := GenerateCosmosDBKey(parsedID) - if err != nil { - return nil, err - } - - entity := &ResourceEntity{} - _, err = c.client.GetDocument(ctx, c.options.DatabaseName, c.options.CollectionName, docID, ops, entity) - - if err != nil && strings.EqualFold(err.Error(), errResourceNotFoundMsg) { - return nil, &store.ErrNotFound{ID: id} - } - - obj := &store.Object{ - Metadata: store.Metadata{ - ID: entity.ResourceID, - ETag: entity.ETag, - }, - Data: entity.Entity, - } - - return obj, err -} - -// Delete parses the given ID, gets the partition key, generates the CosmosDB key, and deletes the document from the -// CosmosDB collection. It returns an error if the document is not found. -func (c *CosmosDBStorageClient) Delete(ctx context.Context, id string, opts ...store.DeleteOptions) error { - parsedID, err := resources.Parse(id) - if err != nil { - return err - } - - partitionKey, err := GetPartitionKey(parsedID) - if err != nil { - return err - } - - ops := cosmosapi.DeleteDocumentOptions{ - PartitionKeyValue: partitionKey, - } - - docID, err := GenerateCosmosDBKey(parsedID) - if err != nil { - return err - } - - _, err = c.client.DeleteDocument(ctx, c.options.DatabaseName, c.options.CollectionName, docID, ops) - if err != nil && strings.EqualFold(err.Error(), errResourceNotFoundMsg) { - return &store.ErrNotFound{ID: id} - } - - return err -} - -// Save saves an object to the CosmosDB storage, returning an error if one occurs. If an ETag is provided, an error is -// returned if the ETag does not match the existing ETag. -func (c *CosmosDBStorageClient) Save(ctx context.Context, obj *store.Object, opts ...store.SaveOptions) error { - if ctx == nil { - return &store.ErrInvalid{Message: "invalid argument. 'ctx' is required"} - } - if obj == nil { - return &store.ErrInvalid{Message: "invalid argument. 'obj' is required"} - } - - cfg := store.NewSaveConfig(opts...) - - parsed, err := resources.Parse(obj.ID) - if err != nil { - return err - } - - docID, err := GenerateCosmosDBKey(parsed) - if err != nil { - return err - } - - partitionKey, err := GetPartitionKey(parsed) - if err != nil { - return err - } - - entity := &ResourceEntity{ - ID: docID, - ResourceID: strings.ToLower(parsed.String()), - RootScope: strings.ToLower(parsed.RootScope()), - PartitionKey: partitionKey, - Entity: obj.Data, - } - - ifMatch := cfg.ETag - if ifMatch == "" && obj.ETag != "" { - ifMatch = obj.ETag - } - - var resp *cosmosapi.Resource - if ifMatch == "" { - op := cosmosapi.CreateDocumentOptions{ - PartitionKeyValue: partitionKey, - IsUpsert: true, - } - resp, _, err = c.client.CreateDocument(ctx, c.options.DatabaseName, c.options.CollectionName, entity, op) - } else { - op := cosmosapi.ReplaceDocumentOptions{ - PartitionKeyValue: partitionKey, - IfMatch: ifMatch, - } - resp, _, err = c.client.ReplaceDocument(ctx, c.options.DatabaseName, c.options.CollectionName, entity.ID, entity, op) - - // TODO: use the response code when switching to official SDK. - if err != nil && strings.HasPrefix(err.Error(), errEtagPreconditionMsgPrefix) { - return &store.ErrConcurrency{} - } - } - - if err != nil { - return err - } - - obj.ETag = resp.Etag - - return nil -} - -// GetPartitionKey returns a partition key based on the given ID, normalizing the subscription ID and normalizing the -// plane namespace if the ID is UCP-qualified. -// Examples: -// /planes/radius/local/... - Partition Key: radius/local -// subscriptions/{guid}/... - Partition Key: {guid} -func GetPartitionKey(id resources.ID) (string, error) { - partitionKey := NormalizeSubscriptionID(id.FindScope(resources_azure.ScopeSubscriptions)) - - if id.IsUCPQualified() { - partitionKey = NormalizeLetterOrDigitToUpper(id.PlaneNamespace()) - } - - return partitionKey, nil -} diff --git a/pkg/ucp/store/cosmosdb/cosmosdbstorageclient_test.go b/pkg/ucp/store/cosmosdb/cosmosdbstorageclient_test.go deleted file mode 100644 index 08a4d5340d..0000000000 --- a/pkg/ucp/store/cosmosdb/cosmosdbstorageclient_test.go +++ /dev/null @@ -1,786 +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 cosmosdb - -import ( - "context" - "fmt" - "os" - "strconv" - "strings" - "testing" - - "github.com/google/uuid" - v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" - "github.com/radius-project/radius/pkg/corerp/datamodel" - rpv1 "github.com/radius-project/radius/pkg/rp/v1" - "github.com/radius-project/radius/pkg/ucp/resources" - "github.com/radius-project/radius/pkg/ucp/store" - "github.com/stretchr/testify/require" - "github.com/vippsas/go-cosmosdb/cosmosapi" -) - -var randomSubscriptionIDs = []string{ - "eaf9116d-84e7-4720-a841-67ca2b67f888", - "7826d962-510f-407a-92a2-5aeb37aa7b6e", - "b2c7913e-e1fe-4c1d-a843-212159d07e46", -} -var randomResourceGroups = []string{ - "red-group", - "blue-group", - "radius-lala", -} -var randomPlanes = []string{ - "local", - "k8s", - "azure", -} - -var ( - // To run this test, you need to specify the below environment variable before running the test. - dBUrl = os.Getenv("TEST_COSMOSDB_URL") - masterKey = os.Getenv("TEST_COSMOSDB_MASTERKEY") - - dbName = "applicationscore" - dbCollectionName = "functional-test-environments" - - testLocation = "test-location" - environmentResourceType = "applications.core/environments" -) - -func getTestEnvironmentModel(rootScope string, resourceName string) *datamodel.Environment { - testID := rootScope + "/providers/applications.core/environments/" + resourceName - - env := &datamodel.Environment{ - BaseResource: v1.BaseResource{ - TrackedResource: v1.TrackedResource{ - ID: testID, - Name: resourceName, - Type: environmentResourceType, - Location: testLocation, - }, - InternalMetadata: v1.InternalMetadata{}, - }, - Properties: datamodel.EnvironmentProperties{ - Compute: rpv1.EnvironmentCompute{ - Kind: rpv1.KubernetesComputeKind, - KubernetesCompute: rpv1.KubernetesComputeProperties{ - ResourceID: "/subscriptions/00000000-0000-0000-1000-000000000001/resourceGroups/testGroup/providers/Microsoft.ContainerService/managedClusters/radiusTestCluster", - Namespace: "default", - }, - }, - }, - } - - env.InternalMetadata.CreatedAPIVersion = "2023-10-01-preview" - env.InternalMetadata.UpdatedAPIVersion = "2023-10-01-preview" - - return env -} - -var dbClient *CosmosDBStorageClient - -func mustGetTestClient(t *testing.T) *CosmosDBStorageClient { - if dBUrl == "" || masterKey == "" { - t.Skip("TEST_COSMOSDB_URL and TEST_COSMOSDB_MASTERKEY are not set.") - } - - if dbClient != nil { - return dbClient - } - - var err error - dbClient, err = NewCosmosDBStorageClient(&ConnectionOptions{ - Url: dBUrl, - DatabaseName: dbName, - CollectionName: dbCollectionName, - MasterKey: masterKey, - }) - if err != nil { - panic(err) - } - - if err := dbClient.Init(context.Background()); err != nil { - panic(err) - } - - return dbClient -} - -func TestConstructCosmosDBQuery(t *testing.T) { - tests := []struct { - desc string - storeQuery store.Query - queryString string - params []cosmosapi.QueryParam - err error - }{ - { - desc: "invalid-query-parameters", - storeQuery: store.Query{}, - err: &store.ErrInvalid{Message: "RootScope can not be empty."}, - }, - { - desc: "scope-recursive-and-routing-scope-prefix", - storeQuery: store.Query{RootScope: "/subscriptions/00000000-0000-0000-1000-000000000001", RoutingScopePrefix: "prefix"}, - err: &store.ErrInvalid{Message: "RoutingScopePrefix is not supported."}, - }, - { - desc: "root-scope-subscription-id", - storeQuery: store.Query{RootScope: "/subscriptions/00000000-0000-0000-1000-000000000001", ScopeRecursive: true}, - queryString: "SELECT * FROM c WHERE STARTSWITH(c.rootScope, @rootScope, true)", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001", - }}, - err: nil, - }, - { - desc: "root-scope-plane", - storeQuery: store.Query{RootScope: "/planes/radius/local", ScopeRecursive: true}, - queryString: "SELECT * FROM c WHERE STARTSWITH(c.rootScope, @rootScope, true)", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/planes/radius/local", - }}, - err: nil, - }, - { - desc: "root-scope-subscription-id-and-resource-group", - storeQuery: store.Query{RootScope: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", ScopeRecursive: false}, - queryString: "SELECT * FROM c WHERE c.rootScope = @rootScope", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", - }}, - err: nil, - }, - - { - desc: "root-scope-plane-and-resource-group", - storeQuery: store.Query{RootScope: "/planes/radius/local/resourcegroups/testgroup", ScopeRecursive: false}, - queryString: "SELECT * FROM c WHERE c.rootScope = @rootScope", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/planes/radius/local/resourcegroups/testgroup", - }}, - err: nil, - }, - { - storeQuery: store.Query{ - RootScope: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", - ResourceType: "applications.core/environments", - }, - queryString: "SELECT * FROM c WHERE c.rootScope = @rootScope and STRINGEQUALS(c.entity.type, @rtype, true)", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", - }, { - Name: "@rtype", - Value: "applications.core/environments", - }}, - err: nil, - }, - { - storeQuery: store.Query{ - RootScope: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", - ResourceType: "applications.core/environments", - Filters: []store.QueryFilter{ - { - Field: "properties.environment", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup/providers/applications.core/environments/env0", - }, - { - Field: "properties.application", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup/providers/applications.core/applications/app0", - }, - }, - }, - queryString: "SELECT * FROM c WHERE c.rootScope = @rootScope and STRINGEQUALS(c.entity.type, @rtype, true) and STRINGEQUALS(c.entity.properties.environment, @filter0, true) and STRINGEQUALS(c.entity.properties.application, @filter1, true)", - params: []cosmosapi.QueryParam{{ - Name: "@rootScope", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup", - }, { - Name: "@rtype", - Value: "applications.core/environments", - }, { - Name: "@filter0", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup/providers/applications.core/environments/env0", - }, { - Name: "@filter1", - Value: "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testgroup/providers/applications.core/applications/app0", - }}, - err: nil, - }, - } - for _, tc := range tests { - t.Run(tc.desc, func(t *testing.T) { - qry, err := constructCosmosDBQuery(tc.storeQuery) - if tc.err != nil { - require.ErrorIs(t, tc.err, err) - } else { - require.Equal(t, tc.queryString, qry.Query) - require.ElementsMatch(t, tc.params, qry.Params) - } - }) - } -} - -func TestGetNotFound(t *testing.T) { - ctx := context.Background() - client := mustGetTestClient(t) - - resourceID := "/subscriptions/00000000-0000-0000-1000-000000000001/resourceGroups/testGroup/providers/applications.core/environments/notfound" - _, err := client.Get(ctx, resourceID) - require.ErrorIs(t, &store.ErrNotFound{ID: resourceID}, err) -} - -func TestDeleteNotFound(t *testing.T) { - ctx := context.Background() - client := mustGetTestClient(t) - - resourceID := "/subscriptions/00000000-0000-0000-1000-000000000001/resourceGroups/testGroup/providers/applications.core/environments/notfound" - err := client.Delete(ctx, resourceID) - require.ErrorIs(t, &store.ErrNotFound{ID: resourceID}, err) -} - -func TestSave(t *testing.T) { - ctx := context.Background() - client := mustGetTestClient(t) - - ucpRootScope := fmt.Sprintf("/planes/radius/local/resourcegroups/%s", randomResourceGroups[0]) - ucpResource := getTestEnvironmentModel(ucpRootScope, "test-UCP-resource") - - armResourceRootScope := fmt.Sprintf("/subscriptions/%s/resourcegroups/%s", randomSubscriptionIDs[0], randomResourceGroups[0]) - armResource := getTestEnvironmentModel(armResourceRootScope, "test-Resource") - - setupTest := func(tb testing.TB, resource *datamodel.Environment) (func(tb testing.TB), *store.Object) { - // Prepare DB object - obj := &store.Object{ - Metadata: store.Metadata{ - ID: resource.ID, - }, - Data: resource, - } - - // Save the object - err := client.Save(ctx, obj) - require.NoError(tb, err) - require.NotEmpty(tb, obj.ETag) - - // Return teardown func and the object - return func(tb testing.TB) { - // Delete object if it exists - err = client.Delete(ctx, resource.ID) - require.NoError(tb, err) - }, obj - } - - // useObjEtag lets you use the existing object etag - tests := map[string]struct { - resource *datamodel.Environment - useObjEtag bool - etag string - useOpts bool - err error - }{ - "upsert-ucp-resource-without-etag": { - resource: ucpResource, - useObjEtag: false, - etag: "", - useOpts: false, - err: nil, - }, - "upsert-arm-resource-without-etag": { - resource: armResource, - useObjEtag: false, - etag: "", - useOpts: false, - err: nil, - }, - "upsert-ucp-resource-with-valid-etag": { - resource: ucpResource, - useObjEtag: true, - etag: "", - useOpts: false, - err: nil, - }, - "upsert-arm-resource-with-valid-etag": { - resource: armResource, - useObjEtag: true, - etag: "", - useOpts: false, - err: nil, - }, - "upsert-ucp-resource-with-options": { - resource: ucpResource, - useObjEtag: false, - etag: "", - useOpts: true, - err: nil, - }, - "upsert-arm-resource-with-options": { - resource: armResource, - useObjEtag: false, - etag: "", - useOpts: true, - err: nil, - }, - "upsert-ucp-resource-with-invalid-etag": { - resource: ucpResource, - useObjEtag: false, - etag: "invalid-etag", - useOpts: false, - err: &store.ErrConcurrency{}, - }, - "upsert-arm-resource-with-invalid-etag": { - resource: armResource, - useObjEtag: false, - etag: "invalid-etag", - useOpts: false, - err: &store.ErrConcurrency{}, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - teardownTest, obj := setupTest(t, tc.resource) - defer teardownTest(t) - - // Update the etag - if !tc.useObjEtag { - obj.ETag = tc.etag - } - - // Upsert the object - var err error - if tc.useOpts { - err = client.Save(ctx, obj, store.WithETag(obj.ETag)) - } else { - err = client.Save(ctx, obj) - } - - // Error checking - if tc.err != nil { - require.ErrorIs(t, err, tc.err) - } else { - require.NoError(t, err) - } - }) - } -} - -// TestQuery tests the following scenarios: -// - Query records by subscription -// - Query records by plane -// - Query records by subscription and resource group -// - Query records by plane and resource group -// - Query records by subscription and resource type -// - Query records by subscription, resource group, and resource type -// - Query records by subscription, resource group, and custom filter -// - Query records by resource type and custom filter (across subscription) -// - Use case - this will be used when environment queries all linked applications and links. -func TestQuery(t *testing.T) { - ctx := context.Background() - client := mustGetTestClient(t) - - ucpResources := []string{} - armResources := []string{} - - // TODO: UCP doesn't check for the plane type - // Ex: /planes/radius/azure/resourcegroups/rg/.../environments/env - // is equal to /planes/radius/local/resourcegroups/rg/.../environments/env - - setupTest := func() func(tb testing.TB) { - // Reset arrays each time - ucpResources = []string{} - armResources = []string{} - - // Creates ucp resources under 3 different planes and 3 different resource groups. - // Total makes 9 ucp resources - for _, plane := range randomPlanes { - for _, resourceGroup := range randomResourceGroups { - // Create and Save a UCP Resource - ucpRootScope := fmt.Sprintf("/planes/radius/%s/resourcegroups/%s", plane, resourceGroup) - ucpEnv := buildAndSaveTestModel(ctx, t, ucpRootScope, uuid.New().String()) - ucpResources = append(ucpResources, ucpEnv.ID) - } - } - - // Creates ARM resources under 3 different subscriptions and 3 different resource groups. - // Total makes 9 ARM resources - for _, subscriptionID := range randomSubscriptionIDs { - for idx, resourceGroup := range randomResourceGroups { - // Create and Save an ARM Resource - armResourceRootScope := fmt.Sprintf("/subscriptions/%s/resourcegroups/%s", subscriptionID, resourceGroup) - armEnv := buildAndSaveTestModel(ctx, t, armResourceRootScope, fmt.Sprintf("test-env-%d", idx)) - armResources = append(armResources, armEnv.ID) - } - } - - // Return teardown - return func(tb testing.TB) { - // Delete all UCP resources after each test - for i := 0; i < len(ucpResources); i++ { - ucpResourceID := ucpResources[i] - err := client.Delete(ctx, ucpResourceID) - require.NoError(tb, err) - } - - // Delete all ARM resources after each test - for i := 0; i < len(armResources); i++ { - armResourceID := armResources[i] - err := client.Delete(ctx, armResourceID) - require.NoError(tb, err) - } - } - } - - queryTest := func(resourceID string, resourceType string, filters []store.QueryFilter, itemsLen int) { - parsedID, err := resources.Parse(resourceID) - require.NoError(t, err) - - // Build the query for testing - query := store.Query{ - RootScope: parsedID.RootScope(), - } - if resourceType != "" { - query.ResourceType = resourceType - } - if len(filters) > 0 { - query.Filters = filters - } - - results, err := client.Query(ctx, query) - require.NoError(t, err) - require.NotNil(t, results) - require.NotNil(t, results.Items) - require.Equal(t, itemsLen, len(results.Items)) - } - - // Query with subscriptionID + resourceGroup - tests := map[string]struct { - resourceType string - filters []store.QueryFilter - expected int - }{ - "just-root-scope": { - resourceType: "", - filters: []store.QueryFilter{}, - expected: 1, - }, - "root-scope-with-resource-type": { - resourceType: environmentResourceType, - filters: []store.QueryFilter{}, - expected: 1, - }, - "root-scope-resource-type-location-filter": { - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: testLocation, - }, - }, - expected: 1, - }, - "root-scope-resource-type-wrong-location-filter": { - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: "wrong-location", - }, - }, - expected: 0, - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - teardownTest := setupTest() - defer teardownTest(t) - - // Query each ucp resource - for i := 0; i < len(ucpResources); i++ { - ucpResource := ucpResources[i] - queryTest(ucpResource, tc.resourceType, tc.filters, tc.expected) - } - - // Query each ARM resource - for i := 0; i < len(armResources); i++ { - armResource := armResources[i] - queryTest(armResource, tc.resourceType, tc.filters, tc.expected) - } - }) - } - - // Query with subscriptionID or plane - These are recursive queries - subscriptionIDCases := map[string]struct { - rootScope string - resourceType string - filters []store.QueryFilter - expected int - }{ - "arm-resource-subscription-id": { - rootScope: fmt.Sprintf("/subscriptions/%s", randomSubscriptionIDs[0]), - resourceType: "", - filters: []store.QueryFilter{}, - expected: 3, - }, - "ucp-resource-subscription-id": { - rootScope: "/planes/radius/local", - resourceType: "", - filters: []store.QueryFilter{}, - expected: 3, - }, - "arm-resource-subscription-id-with-resource-type": { - rootScope: fmt.Sprintf("/subscriptions/%s", randomSubscriptionIDs[0]), - resourceType: environmentResourceType, - filters: []store.QueryFilter{}, - expected: 3, - }, - "ucp-resource-subscription-id-with-resource-type": { - rootScope: "/planes/radius/local", - resourceType: environmentResourceType, - filters: []store.QueryFilter{}, - expected: 3, - }, - "arm-resource-subscription-id-with-resource-type-with-filter": { - rootScope: fmt.Sprintf("/subscriptions/%s", randomSubscriptionIDs[0]), - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: testLocation, - }, - }, - expected: 3, - }, - "ucp-resource-subscription-id-with-resource-type-with-filter": { - rootScope: "/planes/radius/local", - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: testLocation, - }, - }, - expected: 3, - }, - "arm-resource-subscription-id-with-resource-type-with-invalid-filter": { - rootScope: fmt.Sprintf("/subscriptions/%s", randomSubscriptionIDs[0]), - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: "wrong-location", - }, - }, - expected: 0, - }, - "ucp-resource-subscription-id-with-resource-type-with-invalid-filter": { - rootScope: "/planes/radius/local", - resourceType: environmentResourceType, - filters: []store.QueryFilter{ - { - Field: "location", - Value: "wrong-location", - }, - }, - expected: 0, - }, - } - for name, tc := range subscriptionIDCases { - t.Run(name, func(t *testing.T) { - teardownTest := setupTest() - defer teardownTest(t) - - // Build the query for testing - query := store.Query{ - RootScope: tc.rootScope, - ScopeRecursive: true, - } - if tc.resourceType != "" { - query.ResourceType = tc.resourceType - } - if len(tc.filters) > 0 { - query.Filters = tc.filters - } - - results, err := client.Query(ctx, query) - require.NoError(t, err) - require.NotNil(t, results) - require.NotNil(t, results.Items) - require.Equal(t, tc.expected, len(results.Items)) - - for _, item := range results.Items { - if !strings.HasPrefix(item.Metadata.ID, tc.rootScope) { - require.Failf(t, "Matched an item that doesn't include the rootscope %s", item.ID) - } - } - }) - } -} - -// TestPaginationTokenAndQueryItemCount tests the pagination scenario using continuation token and query item count. -func TestPaginationTokenAndQueryItemCount(t *testing.T) { - ctx := context.Background() - client := mustGetTestClient(t) - - ucpResources := []string{} - armResources := []string{} - - ucpRootScope := "/planes/radius/local/resourcegroups/test-RG" - armResourceRootScope := "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/test-RG" - - setupTest := func() func(tb testing.TB) { - // 50 UCP - 50 ARM - for i := 0; i < 50; i++ { - ucpEnv := buildAndSaveTestModel(ctx, t, ucpRootScope, fmt.Sprintf("ucp-env-%d", i)) - ucpResources = append(ucpResources, ucpEnv.ID) - - armEnv := buildAndSaveTestModel(ctx, t, armResourceRootScope, fmt.Sprintf("test-ENV-%d", i)) - armResources = append(armResources, armEnv.ID) - } - - // Return teardown - return func(tb testing.TB) { - for i := 0; i < 50; i++ { - ucpResourceID := ucpResources[i] - err := client.Delete(ctx, ucpResourceID) - require.NoError(tb, err) - - armResourceID := armResources[i] - err = client.Delete(ctx, armResourceID) - require.NoError(tb, err) - } - } - } - - tests := map[string]struct { - rootScope string - itemCount string - }{ - "ucp-resource-default-query-item-count": { - rootScope: ucpRootScope, - itemCount: "", - }, - "arm-resource-default-query-item-count": { - rootScope: armResourceRootScope, - itemCount: "", - }, - "ucp-resource-10-query-item-count": { - rootScope: strings.ToLower(ucpRootScope), // case-insensitive query - itemCount: "10", - }, - "arm-resource-10-query-item-count": { - rootScope: armResourceRootScope, - itemCount: "10", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - teardownTest := setupTest() - defer teardownTest(t) - - remaining := 50 - queryItemCount := defaultQueryItemCount - paginationToken := "" - - for remaining > 0 { - // Build query options - queryOptions := []store.QueryOptions{} - if tc.itemCount != "" { - ic, err := strconv.Atoi(tc.itemCount) - require.NoError(t, err) - queryOptions = append(queryOptions, store.WithMaxQueryItemCount(ic)) - queryItemCount = ic - } - if paginationToken != "" { - queryOptions = append(queryOptions, store.WithPaginationToken(paginationToken)) - } - - if remaining < queryItemCount { - queryItemCount = remaining - } - - results, err := client.Query(ctx, store.Query{RootScope: tc.rootScope}, queryOptions...) - require.NoError(t, err) - require.Equal(t, queryItemCount, len(results.Items)) - - remaining -= queryItemCount - - if remaining > 0 { - require.NotEmpty(t, results.PaginationToken) - paginationToken = results.PaginationToken - } else { - require.Empty(t, results.PaginationToken) - } - } - }) - } -} - -func TestGetPartitionKey(t *testing.T) { - cases := []struct { - desc string - fullID string - out string - }{ - { - "env-partition-key", - "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testGroup/providers/applications.core/environments/env0", - "00000000000000001000000000000001", - }, - { - "env-no-subscription-partition-key", - "/resourcegroups/testGroup/providers/applications.core/environments/env0", - "", - }, - { - "ucp-resource-partition-key-radius-local", - "/planes/radius/local/resourcegroups/testGroup/providers/applications.core/environments/env0", - "RADIUSLOCAL", - }, - { - "ucp-resource-partition-key-radius-k8s", - "/planes/radius/k8s/resourcegroups/testGroup/providers/applications.core/environments/env0", - "RADIUSK8S", - }, - } - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - testID, err := resources.Parse(tc.fullID) - require.NoError(t, err) - key, err := GetPartitionKey(testID) - require.NoError(t, err) - require.Equal(t, tc.out, key) - }) - } -} - -func buildAndSaveTestModel(ctx context.Context, t *testing.T, rootScope string, resourceName string) *datamodel.Environment { - model := getTestEnvironmentModel(rootScope, resourceName) - obj := &store.Object{ - Metadata: store.Metadata{ - ID: model.ID, - }, - Data: model, - } - err := dbClient.Save(ctx, obj) - require.NoError(t, err) - return model -} diff --git a/pkg/ucp/store/cosmosdb/options.go b/pkg/ucp/store/cosmosdb/options.go deleted file mode 100644 index a0dc279aca..0000000000 --- a/pkg/ucp/store/cosmosdb/options.go +++ /dev/null @@ -1,52 +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 cosmosdb - -import "github.com/radius-project/radius/pkg/ucp/store" - -const ( - defaultQueryItemCount = 20 -) - -// ConnectionOptions represents connection info to connect CosmosDB -type ConnectionOptions struct { - // Url represents the url of cosmosdb endpoint. - Url string - // DatabaseName represents the database name to connect. - DatabaseName string - // CollectionName represents the collection name in DataBaseName - CollectionName string - // DefaultQueryItemCount represents the maximum number of items for query. - DefaultQueryItemCount int - // CollectionThroughput represents shared throughput database share the throughput (RU/s) allocated to that database. - CollectionThroughput int - - // MasterKey is the key string for CosmosDB connection. - MasterKey string -} - -func (c *ConnectionOptions) load() error { - if c.MasterKey == "" { - return &store.ErrInvalid{Message: "unset MasterKey"} - } - - if c.DefaultQueryItemCount == 0 { - c.DefaultQueryItemCount = defaultQueryItemCount - } - - return nil -} diff --git a/pkg/ucp/store/cosmosdb/util.go b/pkg/ucp/store/cosmosdb/util.go deleted file mode 100644 index 4af73d0ea3..0000000000 --- a/pkg/ucp/store/cosmosdb/util.go +++ /dev/null @@ -1,154 +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 cosmosdb - -import ( - "errors" - "fmt" - "strings" - "unicode" - - "github.com/radius-project/radius/pkg/ucp/resources" - resources_azure "github.com/radius-project/radius/pkg/ucp/resources/azure" - "github.com/radius-project/radius/pkg/ucp/store" - "github.com/spaolacci/murmur3" -) - -const ( - keyDelimiter = "-" - - // StorageKeyTrimPaddingLen is the length of the padding when key is trimed. - StorageKeyTrimPaddingLen = 17 - // The resource group name storage key length. - ResourceGroupNameMaxStorageKeyLen = 64 - // The resource identifier storage key limit. - ResourceIdMaxStorageKeyLen = 157 -) - -var ( - ErrInvalidKey = errors.New("key includes invalid character") -) - -var escapedStorageKeys = []string{ - ":00", ":01", ":02", ":03", ":04", ":05", ":06", ":07", ":08", ":09", ":0A", ":0B", ":0C", ":0D", ":0E", ":0F", - ":10", ":11", ":12", ":13", ":14", ":15", ":16", ":17", ":18", ":19", ":1A", ":1B", ":1C", ":1D", ":1E", ":1F", - ":20", ":21", ":22", ":23", ":24", ":25", ":26", ":27", ":28", ":29", ":2A", ":2B", ":2C", ":2D", ":2E", ":2F", - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":3A", ":3B", ":3C", ":3D", ":3E", ":3F", - ":40", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", - "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", ":5B", ":5C", ":5D", ":5E", ":5F", - ":60", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", - "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", ":7B", ":7C", ":7D", ":7E", ":7F", -} - -// NormalizeLetterOrDigitToUpper takes in a string and returns a new string with all letters and digits converted to uppercase. -func NormalizeLetterOrDigitToUpper(s string) string { - if s == "" { - return s - } - - sb := strings.Builder{} - for _, ch := range s { - if unicode.IsDigit(ch) || unicode.IsLetter(ch) { - sb.WriteRune(ch) - } - } - - return strings.ToUpper(sb.String()) -} - -// NormalizeSubscriptionID normalizes subscription id. -func NormalizeSubscriptionID(subscriptionID string) string { - return NormalizeLetterOrDigitToUpper(subscriptionID) -} - -// EscapedStorageKey escapes a string so that it can be used as a storage key. -func EscapedStorageKey(key string) string { - sb := strings.Builder{} - for _, ch := range key { - if ch < 128 { - sb.WriteString(escapedStorageKeys[ch]) - } else if unicode.IsDigit(ch) || unicode.IsLetter(ch) { - sb.WriteRune(ch) - } else if ch < 0x100 { - sb.WriteRune(':') - sb.WriteString(fmt.Sprintf("%02d", ch)) - } else { - sb.WriteRune(':') - sb.WriteRune(':') - sb.WriteString(fmt.Sprintf("%04d", ch)) - } - } - return sb.String() -} - -// CombineStorageKeys combines multiple storage keys into one, returning an error if any of the keys contain the key delimiter. -func CombineStorageKeys(keys ...string) (string, error) { - for _, key := range keys { - if strings.Contains(key, keyDelimiter) { - return "", ErrInvalidKey - } - } - - return strings.Join(keys, keyDelimiter), nil -} - -// TrimStorageKey checks if the storage key is too short, contains invalid characters, or exceeds the maximum length, and -// returns a trimmed version of the key or an error if any of these conditions are met. -func TrimStorageKey(storageKey string, maxLength int) (string, error) { - if maxLength < StorageKeyTrimPaddingLen { - return "", &store.ErrInvalid{Message: "storage key is too short"} - } - if strings.Contains(storageKey, "|") { - return "", &store.ErrInvalid{Message: "storage key is not properly encoded"} - } - if len(storageKey) > maxLength { - // Use murmur hash to generate unique key if the length of key exceeds maxLenth - storageKey = fmt.Sprintf("%s|%16X", storageKey[:(maxLength-StorageKeyTrimPaddingLen)], murmur3.Sum64([]byte(storageKey))) - } - return storageKey, nil -} - -// NormalizeStorageKey takes a storage key string and a maximum length and returns a normalized string with -// the maximum length, or an error if the maximum length is exceeded. -func NormalizeStorageKey(storageKey string, maxLength int) (string, error) { - upper := strings.ToUpper(storageKey) - return TrimStorageKey(EscapedStorageKey(upper), maxLength) -} - -// GenerateCosmosDBKey takes in an ID object and returns a string and an error if the resource group or resource type and -// name fail to normalize. -func GenerateCosmosDBKey(id resources.ID) (string, error) { - storageKeys := []string{NormalizeSubscriptionID(id.FindScope(resources_azure.ScopeSubscriptions))} - - resourceGroup := id.FindScope(resources_azure.ScopeResourceGroups) - - if resourceGroup != "" { - uniqueResourceGroup, err := NormalizeStorageKey(resourceGroup, ResourceGroupNameMaxStorageKeyLen) - if err != nil { - return "", err - } - storageKeys = append(storageKeys, uniqueResourceGroup) - } - - resourceTypeAndName, err := NormalizeStorageKey(id.RoutingScope(), ResourceIdMaxStorageKeyLen) - if err != nil { - return "", err - } - storageKeys = append(storageKeys, resourceTypeAndName) - - return CombineStorageKeys(storageKeys...) -} diff --git a/pkg/ucp/store/cosmosdb/util_test.go b/pkg/ucp/store/cosmosdb/util_test.go deleted file mode 100644 index 2a70fa46eb..0000000000 --- a/pkg/ucp/store/cosmosdb/util_test.go +++ /dev/null @@ -1,199 +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 cosmosdb - -import ( - "testing" - - "github.com/radius-project/radius/pkg/ucp/resources" - "github.com/radius-project/radius/pkg/ucp/store" - "github.com/stretchr/testify/require" -) - -func TestNormalizeLetterOrDigitToUpper(t *testing.T) { - testStrings := []struct { - in string - out string - }{ - {"00000000-0000-0000-1000-000000000001", "00000000000000001000000000000001"}, - {"test-GROUp", "TESTGROUP"}, - {"WEST US", "WESTUS"}, - } - - for _, tc := range testStrings { - t.Run(tc.in, func(t *testing.T) { - result := NormalizeLetterOrDigitToUpper(tc.in) - require.Equal(t, tc.out, result) - }) - } -} - -func TestSubscriptionID(t *testing.T) { - testStrings := []struct { - in string - out string - }{ - {"00000000-0000-0000-1000-000000000001", "00000000000000001000000000000001"}, - {"eaf9116d-84e7-4720-a841-67ca2b67f888", "EAF9116D84E74720A84167CA2B67F888"}, - {"b2c7913e-e1fe-4c1d-a843-212159d07e46", "B2C7913EE1FE4C1DA843212159D07E46"}, - } - - for _, tc := range testStrings { - t.Run(tc.in, func(t *testing.T) { - result := NormalizeSubscriptionID(tc.in) - require.Equal(t, tc.out, result) - }) - } -} - -func TestEscapedStorageKey(t *testing.T) { - escapedTests := []struct { - in string - out string - }{ - {"testgroup", "testgroup"}, - {"test-group", "test:2Dgroup"}, - {"/subscriptions/sub/resourceGroups/rgname", ":2Fsubscriptions:2Fsub:2FresourceGroups:2Frgname"}, - } - - for _, tc := range escapedTests { - t.Run(tc.in, func(t *testing.T) { - escaped := EscapedStorageKey(tc.in) - require.Equal(t, tc.out, escaped) - }) - } -} - -func TestTrimStorageKey(t *testing.T) { - trimTests := []struct { - in string - len int - out string - err error - }{ - {"subscripti", 10, "", &store.ErrInvalid{Message: "storage key is too short"}}, - {"subscriptions|0000000000000000|testGroup", StorageKeyTrimPaddingLen, "", &store.ErrInvalid{Message: "storage key is not properly encoded"}}, - {"subscriptions/0000000000000000/testGroup", StorageKeyTrimPaddingLen, "|DCE4A54F0A69CD0F", nil}, - {"subscriptions/00000000000000001000000000000001/resourceGroups/testGroup", 20, "sub|DB99FE979E7C972C", nil}, - {"subscriptions/00000000000000001000000000000001/resourceGroups/testGroup", 80, "subscriptions/00000000000000001000000000000001/resourceGroups/testGroup", nil}, - } - - for _, tc := range trimTests { - t.Run(tc.in, func(t *testing.T) { - trimed, err := TrimStorageKey(tc.in, tc.len) - require.ErrorIs(t, err, tc.err) - require.Equal(t, tc.out, trimed) - }) - } -} - -func TestNormalizeStorageKey(t *testing.T) { - trimTests := []struct { - in string - len int - out string - err error - }{ - {"subscripti", 10, "", &store.ErrInvalid{Message: "storage key is too short"}}, - {"subscriptions/0000000000000000/testGroup", StorageKeyTrimPaddingLen, "|7A4B44E13072BE17", nil}, - {"subscriptions/00000000000000001000000000000001/resourceGroups/testGroup", 20, "SUB|10844510550A50BD", nil}, - {"subscriptions/00000000000000001000000000000001/resourceGroups/testGroup", 80, "SUBSCRIPTIONS:2F00000000000000001000000000000001:2FRESOURCEGROUPS:2FTESTGROUP", nil}, - } - - for _, tc := range trimTests { - t.Run(tc.in, func(t *testing.T) { - trimed, err := NormalizeStorageKey(tc.in, tc.len) - require.ErrorIs(t, err, tc.err) - require.Equal(t, tc.out, trimed) - }) - } -} - -func TestGenerateCosmosDBKey(t *testing.T) { - cases := []struct { - desc string - fullID string - out string - err error - }{ - { - "env-success-1", - "/subscriptions/00000000-0000-0000-1000-000000000001/resourcegroups/testGroup/providers/applications.core/environments/env0", - "00000000000000001000000000000001-TESTGROUP-APPLICATIONS:2ECORE:2FENVIRONMENTS:2FENV0", - nil, - }, - { - "env-success-2", - "/subscriptions/eaf9116d-84e7-4720-a841-67ca2b67f888/resourcegroups/testGroup/providers/Applications.Core/environments/appenv", - "EAF9116D84E74720A84167CA2B67F888-TESTGROUP-APPLICATIONS:2ECORE:2FENVIRONMENTS:2FAPPENV", - nil, - }, - { - "env-no-rg-success", - "/subscriptions/00000000-0000-0000-1000-000000000001/providers/Applications.Core/environments/env0", - "00000000000000001000000000000001-APPLICATIONS:2ECORE:2FENVIRONMENTS:2FENV0", - nil, - }, - { - "os-success", - "/subscriptions/00000000-0000-0000-1000-000000000001/providers/Applications.Core/locations/westus/operationStatuses/os1", - "00000000000000001000000000000001-APPLICATIONS:2ECORE:2FLOCATIONS:2FWESTUS:2FOPERATIONSTATUSES:2FOS1", - nil, - }, - { - "app-success", - "/subscriptions/7826d962-510f-407a-92a2-5aeb37aa7b6e/resourcegroups/radius-westus/providers/Applications.Core/applications/todoapp", - "7826D962510F407A92A25AEB37AA7B6E-RADIUS:2DWESTUS-APPLICATIONS:2ECORE:2FAPPLICATIONS:2FTODOAPP", - nil, - }, - { - "app-long-name-success", - "/subscriptions/7826d962-510f-407a-92a2-5aeb37aa7b6e/resourcegroups/radius-westus/providers/Applications.Core/applications/longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1", - "7826D962510F407A92A25AEB37AA7B6E-RADIUS:2DWESTUS-APPLICATIONS:2ECORE:2FAPPLICATIONS:2FLONGAPPLICATIONNAME1LONGAPPLICATIONNAME1LONGAPPLICATIONNAME1LONGAPPLICATIONNAME1LONGAPPLICATIONNAME1LON|651E511DBBDDC783", - nil, - }, - { - "app-long-resource-name-success", - "/subscriptions/7826d962-510f-407a-92a2-5aeb37aa7b6e/resourcegroups/radius-westus/providers/Applications.Core/longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0/longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1", - "7826D962510F407A92A25AEB37AA7B6E-RADIUS:2DWESTUS-APPLICATIONS:2ECORE:2FLONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME|279366913EF52FC7", - nil, - }, - { - "app-long-rg-app-names-success", - "/subscriptions/7826d962-510f-407a-92a2-5aeb37aa7b6e/resourcegroups/longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0longresourcegroup0/providers/Applications.Core/longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0longresourcename0/longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1longapplicationname1", - "7826D962510F407A92A25AEB37AA7B6E-LONGRESOURCEGROUP0LONGRESOURCEGROUP0LONGRESOURC|EF662FD5E8286859-APPLICATIONS:2ECORE:2FLONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME0LONGRESOURCENAME|279366913EF52FC7", - nil, - }, - { - "ucp-success", - "/planes/radius/local/resourcegroups/radius-westus/providers/Applications.Core/applications/todoapp", - "-RADIUS:2DWESTUS-APPLICATIONS:2ECORE:2FAPPLICATIONS:2FTODOAPP", - nil, - }, - } - - for _, tc := range cases { - t.Run(tc.desc, func(t *testing.T) { - testID, err := resources.Parse(tc.fullID) - require.NoError(t, err) - key, err := GenerateCosmosDBKey(testID) - require.ErrorIs(t, err, tc.err) - require.Equal(t, tc.out, key) - require.LessOrEqual(t, len(key), 255) - }) - } -}