From 3f4311a3a81b3923ad7fa53f78275c18100ae286 Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Mon, 5 Aug 2024 11:09:07 +0200 Subject: [PATCH] Merge branch 'beta' (#182) * Add support specifying Azure resource groups for RSC features (#160) * Add retry to allVpcsByRegion request (#162) * Move the retry handling into the Request function (#165) * Expose account name and FQDN (#166) * Add support for Azure shared exocompute (#167) * Fix an issue with AWS exocompute customer supplied security groups (#168) * Add support for Azure archival locations (#169) * Fix Azure permission upgrade issue (#170) * Increase the korg job wait attempts to 50 (#171) * Skip disabling AWS Exocompute for non-CFT workflow (#172) * Fix CreateStorageSetting's NativeID field (#173) * Use RSC_MANAGED_CLUSTER permission group (#174) * Extend AddClusterToExocomputeConfig to return setup YAML (#175) * Disable S3 protection before removing the feature (#176) * Fix failing Azure integration tests (#177) * Work around an AWS multiple features removal issue (#178) * Add functions to look up cloud accounts by native ID and name (#180) * Add support for updating AWS archival location bucket tags (#179) --- examples/aws_exocompute/main.go | 2 +- examples/aws_shared_exocompute/main.go | 29 +- examples/aws_storage_setting/main.go | 2 +- examples/azure_exocompute/main.go | 2 +- examples/azure_permissions/main.go | 43 +- examples/azure_shared_exocompute/main.go | 95 ++ examples/azure_subscription/main.go | 2 +- go.mod | 2 +- go.sum | 4 +- internal/testsetup/testsetup.go | 42 +- pkg/polaris/access/role.go | 8 +- pkg/polaris/aws/{storage.go => archival.go} | 70 +- pkg/polaris/aws/aws.go | 156 +++- pkg/polaris/aws/exocompute.go | 158 ++-- pkg/polaris/aws/options.go | 16 +- pkg/polaris/azure/archival.go | 271 ++++++ pkg/polaris/azure/azure.go | 545 ++++++----- pkg/polaris/azure/azure_test.go | 457 ++++++--- pkg/polaris/azure/exocompute.go | 186 +++- pkg/polaris/azure/exocompute_test.go | 104 ++- pkg/polaris/azure/options.go | 60 +- pkg/polaris/azure/permissions.go | 156 +++- pkg/polaris/azure/principal.go | 302 +++--- pkg/polaris/azure/principal_test.go | 123 +++ ..._azure_cloud_account_tenants_response.json | 270 ++++++ pkg/polaris/azure/testdata/key_file_v0.json | 7 + pkg/polaris/azure/testdata/key_file_v1.json | 7 + pkg/polaris/azure/testdata/key_file_v2.json | 6 + pkg/polaris/config.go | 520 ++++++----- pkg/polaris/config_test.go | 30 +- pkg/polaris/gcp/gcp.go | 22 +- pkg/polaris/graphql/archival/archival.go | 179 ++++ pkg/polaris/graphql/archival/queries.go | 32 + .../queries/delete_target_mapping.graphql | 5 + pkg/polaris/graphql/aws/archival.go | 132 +++ pkg/polaris/graphql/aws/aws.go | 38 +- pkg/polaris/graphql/aws/aws_test.go | 16 +- pkg/polaris/graphql/aws/cloud_test.go | 2 +- pkg/polaris/graphql/aws/exocompute.go | 339 +++---- pkg/polaris/graphql/aws/queries.go | 101 +- .../all_aws_exocompute_configs.graphql | 4 +- .../aws_exocompute_cluster_connect.graphql | 1 + .../create_aws_exocompute_configs.graphql | 36 +- .../delete_aws_exocompute_configs.graphql | 2 +- .../disconnect_aws_exocompute_cluster.graphql | 2 +- .../update_aws_exocompute_configs.graphql | 36 +- ...e_cloud_native_aws_storage_setting.graphql | 20 +- pkg/polaris/graphql/aws/storage.go | 225 ----- ...nd_create_aws_cloud_account_response.json} | 0 pkg/polaris/graphql/azure/archival.go | 149 +++ pkg/polaris/graphql/azure/azure.go | 184 +--- pkg/polaris/graphql/azure/cloud.go | 292 +++--- pkg/polaris/graphql/azure/exocompute.go | 207 +++-- pkg/polaris/graphql/azure/native.go | 19 +- pkg/polaris/graphql/azure/queries.go | 168 +++- .../all_azure_cloud_account_tenants.graphql | 17 + ...zure_exocompute_configs_in_account.graphql | 8 + .../azure/queries/all_target_mappings.graphql | 36 + ...re_cloud_account_permission_config.graphql | 12 +- .../azure_cloud_account_tenant.graphql | 18 - ...cloud_native_azure_storage_setting.graphql | 31 + ...ud_account_exocompute_subscription.graphql | 8 + ...ud_account_exocompute_subscription.graphql | 7 + ...cloud_native_azure_storage_setting.graphql | 19 + pkg/polaris/graphql/azure/regions.go | 878 ++++++++++++++++++ .../azure/{azure_test.go => regions_test.go} | 16 +- pkg/polaris/graphql/core/core.go | 125 ++- pkg/polaris/graphql/core/core_test.go | 36 +- ...on => korg_taskchain_status_response.json} | 0 pkg/polaris/graphql/errors_test.go | 2 +- pkg/polaris/graphql/exocompute/exocompute.go | 284 ++++++ pkg/polaris/graphql/exocompute/queries.go | 54 ++ ..._cloud_account_exocompute_mappings.graphql | 6 + ...p_cloud_account_exocompute_account.graphql | 9 + ...p_cloud_account_exocompute_account.graphql | 8 + pkg/polaris/graphql/gcp/cloud.go | 2 +- pkg/polaris/graphql/gcp/gcp.go | 2 +- pkg/polaris/graphql/graphql.go | 50 +- pkg/polaris/graphql/graphql_test.go | 4 +- ...rom_auth.json => auth_error_response.json} | 0 ...aphql.json => graphql_error_response.json} | 0 pkg/polaris/polaris.go | 134 +-- pkg/polaris/token/cache.go | 4 +- pkg/polaris/token/cache_test.go | 2 +- pkg/polaris/token/request.go | 8 +- ..._polaris.json => auth_error_response.json} | 0 pkg/polaris/token/token_test.go | 2 +- pkg/polaris/token/user_source.go | 4 +- 88 files changed, 5372 insertions(+), 2300 deletions(-) create mode 100644 examples/azure_shared_exocompute/main.go rename pkg/polaris/aws/{storage.go => archival.go} (74%) create mode 100644 pkg/polaris/azure/archival.go create mode 100644 pkg/polaris/azure/principal_test.go create mode 100644 pkg/polaris/azure/testdata/all_azure_cloud_account_tenants_response.json create mode 100644 pkg/polaris/azure/testdata/key_file_v0.json create mode 100644 pkg/polaris/azure/testdata/key_file_v1.json create mode 100644 pkg/polaris/azure/testdata/key_file_v2.json create mode 100644 pkg/polaris/graphql/archival/archival.go create mode 100644 pkg/polaris/graphql/archival/queries.go create mode 100644 pkg/polaris/graphql/archival/queries/delete_target_mapping.graphql create mode 100644 pkg/polaris/graphql/aws/archival.go delete mode 100644 pkg/polaris/graphql/aws/storage.go rename pkg/polaris/graphql/aws/testdata/{validate_and_create_aws_cloud_account.json => validate_and_create_aws_cloud_account_response.json} (100%) create mode 100644 pkg/polaris/graphql/azure/archival.go create mode 100644 pkg/polaris/graphql/azure/queries/all_target_mappings.graphql delete mode 100644 pkg/polaris/graphql/azure/queries/azure_cloud_account_tenant.graphql create mode 100644 pkg/polaris/graphql/azure/queries/create_cloud_native_azure_storage_setting.graphql create mode 100644 pkg/polaris/graphql/azure/queries/map_azure_cloud_account_exocompute_subscription.graphql create mode 100644 pkg/polaris/graphql/azure/queries/unmap_azure_cloud_account_exocompute_subscription.graphql create mode 100644 pkg/polaris/graphql/azure/queries/update_cloud_native_azure_storage_setting.graphql create mode 100644 pkg/polaris/graphql/azure/regions.go rename pkg/polaris/graphql/azure/{azure_test.go => regions_test.go} (84%) rename pkg/polaris/graphql/core/testdata/{korgtaskchainstatus.json => korg_taskchain_status_response.json} (100%) create mode 100644 pkg/polaris/graphql/exocompute/exocompute.go create mode 100644 pkg/polaris/graphql/exocompute/queries.go create mode 100644 pkg/polaris/graphql/exocompute/queries/all_cloud_account_exocompute_mappings.graphql create mode 100644 pkg/polaris/graphql/exocompute/queries/map_cloud_account_exocompute_account.graphql create mode 100644 pkg/polaris/graphql/exocompute/queries/unmap_cloud_account_exocompute_account.graphql rename pkg/polaris/graphql/testdata/{error_json_from_auth.json => auth_error_response.json} (100%) rename pkg/polaris/graphql/testdata/{error_graphql.json => graphql_error_response.json} (100%) rename pkg/polaris/token/testdata/{error_json_from_polaris.json => auth_error_response.json} (100%) diff --git a/examples/aws_exocompute/main.go b/examples/aws_exocompute/main.go index f12de793..b2e8d734 100644 --- a/examples/aws_exocompute/main.go +++ b/examples/aws_exocompute/main.go @@ -34,7 +34,7 @@ import ( func main() { ctx := context.Background() - // Load configuration and create client. + // Load configuration and create a client. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { log.Fatal(err) diff --git a/examples/aws_shared_exocompute/main.go b/examples/aws_shared_exocompute/main.go index d30f6121..e1a22f5b 100644 --- a/examples/aws_shared_exocompute/main.go +++ b/examples/aws_shared_exocompute/main.go @@ -1,4 +1,4 @@ -// Copyright 2021 Rubrik, Inc. +// Copyright 2024 Rubrik, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to @@ -36,13 +36,15 @@ import ( func main() { ctx := context.Background() - // Load configuration and create client. + // Load configuration and create a client. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { log.Fatal(err) } logger := polaris_log.NewStandardLogger() - polaris.SetLogLevelFromEnv(logger) + if err := polaris.SetLogLevelFromEnv(logger); err != nil { + log.Fatal(err) + } client, err := polaris.NewClientWithLogger(polAccount, logger) if err != nil { log.Fatal(err) @@ -57,12 +59,11 @@ func main() { // Add the AWS default account to Polaris. Usually resolved using the // environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and // AWS_DEFAULT_REGION. - accountID, err := awsClient.AddAccount(ctx, aws.Default(), - []core.Feature{core.FeatureCloudNativeProtection, core.FeatureExocompute}, aws.Regions("us-east-2")) + accountID, err := awsClient.AddAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, + aws.Regions("us-east-2")) if err != nil { log.Fatal(err) } - fmt.Printf("Account ID: %v\n", accountID) // Map the application account to an existing exocompute host account. @@ -76,21 +77,8 @@ func main() { if err != nil { log.Fatal(err) } - fmt.Printf("Exocompute Host Account: %v\n", hostID) - // Retrieve the exocompute application accounts for the exocompute host - // account. - appIDs, err := awsClient.ExocomputeApplicationAccounts(ctx, hostAccountID) - if err != nil { - log.Fatal(err) - } - - fmt.Println("Exocompute Application Accounts:") - for _, appID := range appIDs { - fmt.Println(appID) - } - // Unmap the application account from the shared exocompute host account. err = awsClient.UnmapExocompute(ctx, aws.CloudAccountID(accountID)) if err != nil { @@ -98,8 +86,7 @@ func main() { } // Remove the AWS account from Polaris. - err = awsClient.RemoveAccount(ctx, aws.Default(), - []core.Feature{core.FeatureCloudNativeProtection, core.FeatureExocompute}, false) + err = awsClient.RemoveAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { log.Fatal(err) } diff --git a/examples/aws_storage_setting/main.go b/examples/aws_storage_setting/main.go index 80c030ef..22f8ec9d 100644 --- a/examples/aws_storage_setting/main.go +++ b/examples/aws_storage_setting/main.go @@ -72,7 +72,7 @@ func main() { fmt.Printf("ID: %v, Name: %s\n", targetMapping.ID, targetMapping.Name) // Update the AWS archival location. - err = awsClient.UpdateStorageSetting(ctx, targetMappingID, "TestUpdated", "", "") + err = awsClient.UpdateStorageSetting(ctx, targetMappingID, "TestUpdated", "", "", nil) if err != nil { log.Fatal(err) } diff --git a/examples/azure_exocompute/main.go b/examples/azure_exocompute/main.go index 4c516b53..6a842d0f 100644 --- a/examples/azure_exocompute/main.go +++ b/examples/azure_exocompute/main.go @@ -35,7 +35,7 @@ import ( func main() { ctx := context.Background() - // Load configuration and create client. + // Load configuration and create a client. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { log.Fatal(err) diff --git a/examples/azure_permissions/main.go b/examples/azure_permissions/main.go index 289233b5..00391f63 100644 --- a/examples/azure_permissions/main.go +++ b/examples/azure_permissions/main.go @@ -25,7 +25,6 @@ import ( "fmt" "log" - "github.com/google/uuid" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/azure" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" @@ -41,7 +40,7 @@ import ( func main() { ctx := context.Background() - // Load configuration and create client. + // Load configuration and create a client. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { log.Fatal(err) @@ -53,36 +52,42 @@ func main() { azureClient := azure.Wrap(client) - // List Azure permissions needed for features. - features := []core.Feature{core.FeatureCloudNativeProtection} - perms, err := azureClient.Permissions(ctx, features) + // List Azure permissions needed for the Cloud Native Protection feature. + perms, permGroups, err := azureClient.ScopedPermissions(ctx, core.FeatureCloudNativeProtection) if err != nil { log.Fatal(err) } - fmt.Println("Permissions requried for Cloud Native Protection:") - for _, perm := range perms.Actions { + fmt.Println("Subscription level permissions required for Cloud Native Protection:") + for _, perm := range perms[azure.ScopeSubscription].Actions { fmt.Println(perm) } - for _, perm := range perms.NotActions { + for _, perm := range perms[azure.ScopeSubscription].NotActions { fmt.Println(perm) } - for _, perm := range perms.DataActions { + for _, perm := range perms[azure.ScopeSubscription].DataActions { fmt.Println(perm) } - for _, perm := range perms.NotDataActions { + for _, perm := range perms[azure.ScopeSubscription].NotDataActions { fmt.Println(perm) } - // Notify Polaris about updated permissions for the Cloud Native Protection - // feature of the already added subscription. - account, err := azureClient.Subscription(ctx, - azure.SubscriptionID(uuid.MustParse("27dce22c-1b84-11ec-9992-a3d4a0eb7b90")), core.FeatureCloudNativeProtection) - if err != nil { - log.Fatal(err) + fmt.Println("Resource group level permissions required for Cloud Native Protection:") + for _, perm := range perms[azure.ScopeResourceGroup].Actions { + fmt.Println(perm) } - err = azureClient.PermissionsUpdated(ctx, azure.CloudAccountID(account.ID), features) - if err != nil { - log.Fatal(err) + for _, perm := range perms[azure.ScopeResourceGroup].NotActions { + fmt.Println(perm) + } + for _, perm := range perms[azure.ScopeResourceGroup].DataActions { + fmt.Println(perm) + } + for _, perm := range perms[azure.ScopeResourceGroup].NotDataActions { + fmt.Println(perm) + } + + fmt.Println("Permission groups available for Cloud Native Protection:") + for _, permGroup := range permGroups { + fmt.Printf("Permission group %s: %d\n", permGroup.Name, permGroup.Version) } } diff --git a/examples/azure_shared_exocompute/main.go b/examples/azure_shared_exocompute/main.go new file mode 100644 index 00000000..2cb4862d --- /dev/null +++ b/examples/azure_shared_exocompute/main.go @@ -0,0 +1,95 @@ +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/azure" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" + polaris_log "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" +) + +// Note: This example requires an existing Azure account with exocompute +// configured in RSC. +func main() { + ctx := context.Background() + + // Load configuration and create a client. + polAccount, err := polaris.DefaultServiceAccount(true) + if err != nil { + log.Fatal(err) + } + logger := polaris_log.NewStandardLogger() + if err := polaris.SetLogLevelFromEnv(logger); err != nil { + log.Fatal(err) + } + client, err := polaris.NewClientWithLogger(polAccount, logger) + if err != nil { + log.Fatal(err) + } + + azureClient := azure.Wrap(client) + + // The AWS account ID of the existing AWS account with exocompute + // configured. + hostAccountID := azure.SubscriptionID(uuid.MustParse("3cad3091-a1b3-4e0e-823d-84589568983e")) + + // Add the AWS default account to Polaris. Usually resolved using the + // environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and + // AWS_DEFAULT_REGION. + subscription := azure.Subscription(uuid.MustParse("e4b247e7-66c5-4f10-9042-1eeac424c7a4"), + "my-domain.onmicrosoft.com") + accountID, err := azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeProtection, azure.Regions("us-east-2")) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Account ID: %v\n", accountID) + + // Map the application account to an existing exocompute host account. + err = azureClient.MapExocompute(ctx, hostAccountID, azure.CloudAccountID(accountID)) + if err != nil { + log.Fatal(err) + } + + // Retrieve the exocompute host account for the application account. + hostID, err := azureClient.ExocomputeHostAccount(ctx, azure.CloudAccountID(accountID)) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Exocompute Host Account: %v\n", hostID) + + // Unmap the application account from the shared exocompute host account. + err = azureClient.UnmapExocompute(ctx, azure.CloudAccountID(accountID)) + if err != nil { + log.Fatal(err) + } + + // Remove the AWS account from Polaris. + err = azureClient.RemoveSubscription(ctx, azure.CloudAccountID(accountID), core.FeatureCloudNativeProtection, false) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/azure_subscription/main.go b/examples/azure_subscription/main.go index c1e24d59..2141b356 100644 --- a/examples/azure_subscription/main.go +++ b/examples/azure_subscription/main.go @@ -40,7 +40,7 @@ import ( func main() { ctx := context.Background() - // Load configuration and create client. + // Load configuration and create the client. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index b9f627c3..f25180be 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect diff --git a/go.sum b/go.sum index f48c319c..36f1d2b8 100644 --- a/go.sum +++ b/go.sum @@ -180,8 +180,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= diff --git a/internal/testsetup/testsetup.go b/internal/testsetup/testsetup.go index ed6b94fe..531762ca 100644 --- a/internal/testsetup/testsetup.go +++ b/internal/testsetup/testsetup.go @@ -9,7 +9,7 @@ import ( ) // testAwsAccount hold AWS account information used in the integration tests. -// Normally used to assert that the information read from Polaris is correct. +// Normally used to assert that the information read from RSC is correct. type testAwsAccount struct { Profile string `json:"profile"` AccountID string `json:"accountId"` @@ -49,24 +49,38 @@ func AWSAccount() (testAwsAccount, error) { // testAzureSubscription hold Azure subscription information used in the // integration tests. Normally used to assert that the information read from -// Polaris is correct. +// RSC is correct. type testAzureSubscription struct { SubscriptionID uuid.UUID `json:"subscriptionId"` SubscriptionName string `json:"subscriptionName"` + TenantID uuid.UUID `json:"tenantId"` TenantDomain string `json:"tenantDomain"` + PrincipalID uuid.UUID `json:"principalId"` + PrincipalName string `json:"principalName"` + PrincipalSecret string `json:"principalSecret"` - Exocompute struct { - SubnetID string `json:"subnetId"` - } `json:"exocompute"` - - // should be in EastUS2 region - // for integration test - // as region is hardcoded there. + // Should be in EastUS2 region for integration test as the region is + // hardcoded there. Archival struct { - ManagedIdentityName string `json:"managedIdentityName"` - PrincipalID string `json:"managedIdentityPrincipalId"` - ResourceGroupName string `json:"resourceGroupName"` + Regions []string `json:"regions"` + ManagedIdentityName string `json:"managedIdentityName"` + PrincipalID string `json:"managedIdentityPrincipalId"` + ResourceGroupName string `json:"resourceGroupName"` + ResourceGroupRegion string `json:"resourceGroupRegion"` } `json:"archival"` + + CloudNativeProtection struct { + Regions []string `json:"regions"` + ResourceGroupName string `json:"resourceGroupName"` + ResourceGroupRegion string `json:"resourceGroupRegion"` + } `json:"cloudNativeProtection"` + + Exocompute struct { + Regions []string `json:"regions"` + ResourceGroupName string `json:"resourceGroupName"` + ResourceGroupRegion string `json:"resourceGroupRegion"` + SubnetID string `json:"subnetId"` + } `json:"exocompute"` } // AzureSubscription loads test project information from the file pointed to by @@ -86,7 +100,7 @@ func AzureSubscription() (testAzureSubscription, error) { } // testGcpProject hold GCP project information used in the integration tests. -// Normally used to assert that the information read from Polaris is correct. +// Normally used to assert that the information read from RSC is correct. type testGcpProject struct { ProjectName string `json:"projectName"` ProjectID string `json:"projectId"` @@ -109,7 +123,7 @@ func GCPProject() (testGcpProject, error) { } // testRSCConfig hold configuration information used in the integration tests. -// Normally used to assert that the information read from Polaris is correct. +// Normally used to assert that the information read from RSC is correct. type testRSCConfig struct { ExistingUserEmail string `json:"existingUserEmail"` NewUserEmail string `json:"newUserEmail"` diff --git a/pkg/polaris/access/role.go b/pkg/polaris/access/role.go index 3ae2fddf..944fa5b2 100644 --- a/pkg/polaris/access/role.go +++ b/pkg/polaris/access/role.go @@ -90,7 +90,7 @@ func (a API) RoleByName(ctx context.Context, name string) (Role, error) { role, err := findRoleByName(roles, name) if err != nil { - return Role{}, fmt.Errorf("failed to find role: %w", err) + return Role{}, err } return role, nil @@ -105,7 +105,7 @@ func findRoleByName(roles []Role, name string) (Role, error) { } } - return Role{}, fmt.Errorf("role with name %q %w", name, graphql.ErrNotFound) + return Role{}, fmt.Errorf("role %q %w", name, graphql.ErrNotFound) } // Roles returns the roles matching the specified role name filter. The name @@ -188,7 +188,7 @@ func (a API) RoleTemplateByName(ctx context.Context, name string) (RoleTemplate, roleTemplate, err := findRoleTemplateByName(roleTemplates, name) if err != nil { - return RoleTemplate{}, fmt.Errorf("failed to find role template: %v", err) + return RoleTemplate{}, err } return roleTemplate, nil @@ -203,7 +203,7 @@ func findRoleTemplateByName(roleTemplates []RoleTemplate, name string) (RoleTemp } } - return RoleTemplate{}, fmt.Errorf("role template with name %q %w", name, graphql.ErrNotFound) + return RoleTemplate{}, fmt.Errorf("role template %q %w", name, graphql.ErrNotFound) } // RoleTemplates returns the role templates matching the specified role template diff --git a/pkg/polaris/aws/storage.go b/pkg/polaris/aws/archival.go similarity index 74% rename from pkg/polaris/aws/storage.go rename to pkg/polaris/aws/archival.go index 7c889d83..799dc963 100644 --- a/pkg/polaris/aws/storage.go +++ b/pkg/polaris/aws/archival.go @@ -26,6 +26,7 @@ import ( "github.com/google/uuid" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/archival" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) @@ -46,7 +47,7 @@ type TargetMapping struct { } // TargetMappingByID returns the AWS target mapping with the specified ID. -// If no target mapping with the specified ID is found graphql.ErrNotFound is +// If no target mapping with the specified ID is found, graphql.ErrNotFound is // returned. func (a API) TargetMappingByID(ctx context.Context, id uuid.UUID) (TargetMapping, error) { a.log.Print(log.Trace) @@ -55,7 +56,7 @@ func (a API) TargetMappingByID(ctx context.Context, id uuid.UUID) (TargetMapping Field: "ARCHIVAL_GROUP_ID", Text: id.String(), }} - targets, err := aws.Wrap(a.client).AllTargetMappings(ctx, filter) + targets, err := archival.ListTargetMappings[aws.TargetMapping](ctx, a.client, filter) if err != nil { return TargetMapping{}, fmt.Errorf("failed to get target mappings: %s", err) } @@ -70,7 +71,7 @@ func (a API) TargetMappingByID(ctx context.Context, id uuid.UUID) (TargetMapping } // TargetMappingByName returns the AWS target mapping with the specified name. -// If no target mapping with the specified ID is found graphql.ErrNotFound is +// If no target mapping with the specified ID is found, graphql.ErrNotFound is // returned. func (a API) TargetMappingByName(ctx context.Context, name string) (TargetMapping, error) { a.log.Print(log.Trace) @@ -79,7 +80,7 @@ func (a API) TargetMappingByName(ctx context.Context, name string) (TargetMappin Field: "NAME", Text: name, }} - targets, err := aws.Wrap(a.client).AllTargetMappings(ctx, filter) + targets, err := archival.ListTargetMappings[aws.TargetMapping](ctx, a.client, filter) if err != nil { return TargetMapping{}, fmt.Errorf("failed to get target mappings: %s", err) } @@ -95,8 +96,8 @@ func (a API) TargetMappingByName(ctx context.Context, name string) (TargetMappin // TargetMappings returns all AWS target mappings that match the specified // archival group and name filter. The name filter can be used to search for -// prefixes of a name. If the name filter is empty is will match all names. -// In RSC cloud archival locations are also referred to as target mappings. +// prefixes of a name. If the name filter is empty, is will match all names. +// In RSC cloud, archival locations are also referred to as target mappings. func (a API) TargetMappings(ctx context.Context, nameFilter string) ([]TargetMapping, error) { a.log.Print(log.Trace) @@ -104,7 +105,7 @@ func (a API) TargetMappings(ctx context.Context, nameFilter string) ([]TargetMap Field: "NAME", Text: nameFilter, }} - targets, err := aws.Wrap(a.client).AllTargetMappings(ctx, filter) + targets, err := archival.ListTargetMappings[aws.TargetMapping](ctx, a.client, filter) if err != nil { return nil, fmt.Errorf("failed to get target mappings: %s", err) } @@ -122,7 +123,7 @@ func (a API) TargetMappings(ctx context.Context, nameFilter string) ([]TargetMap func (a API) DeleteTargetMapping(ctx context.Context, id uuid.UUID) error { a.log.Print(log.Trace) - return aws.Wrap(a.client).DeleteTargetMapping(ctx, id) + return archival.DeleteTargetMapping(ctx, a.client, id) } // CreateStorageSetting creates a cloud native archival location. @@ -136,17 +137,9 @@ func (a API) CreateStorageSetting(ctx context.Context, id IdentityFunc, name, bu return uuid.Nil, err } - tags := make([]aws.Tag, 0, len(bucketTags)) - for key, value := range bucketTags { - tags = append(tags, aws.Tag{Key: key, Value: value}) - } - reg := aws.RegionUnknown if region != "" && region != "UNKNOWN_AWS_REGION" { - reg, err = aws.ParseRegion(region) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to parse region: %s", err) - } + reg = aws.ParseRegionNoValidation(region) } locTemplate := "SPECIFIC_REGION" @@ -154,7 +147,15 @@ func (a API) CreateStorageSetting(ctx context.Context, id IdentityFunc, name, bu locTemplate = "SOURCE_REGION" } - targetMappingID, err := aws.Wrap(a.client).CreateCloudNativeStorageSetting(ctx, cloudAccountID, name, bucketPrefix, storageClass, reg, kmsMasterKey, locTemplate, tags) + targetMappingID, err := archival.CreateCloudNativeStorageSetting[aws.StorageSettingCreateResult](ctx, a.client, cloudAccountID, aws.StorageSettingCreateParams{ + Name: name, + BucketPrefix: bucketPrefix, + StorageClass: storageClass, + Region: reg, + KmsMasterKey: kmsMasterKey, + LocTemplate: locTemplate, + BucketTags: toTagsInput(bucketTags), + }) if err != nil { return uuid.Nil, fmt.Errorf("failed to create cloud native storage setting: %s", err) } @@ -163,20 +164,41 @@ func (a API) CreateStorageSetting(ctx context.Context, id IdentityFunc, name, bu } // UpdateStorageSetting updates the cloud native archival location with the -// specified ID. The KMS master key can be either a key alias or a key ID. -// Note that not all properties can be updated, only the name, storage and KMS -// master key. -func (a API) UpdateStorageSetting(ctx context.Context, id uuid.UUID, name, storageClass, kmsMasterKey string) error { +// specified ID. The KMS master key can be either a key alias or a key ID. The +// bucket tags replace all existing tags. Note that not all properties can be +// updated, only the name, storage class, KMS master key and bucket tags can be +// updated. +func (a API) UpdateStorageSetting(ctx context.Context, targetMappingID uuid.UUID, name, storageClass, kmsMasterKey string, bucketTags map[string]string) error { a.log.Print(log.Trace) - if err := aws.Wrap(a.client).UpdateCloudNativeStorageSetting(ctx, id, name, storageClass, kmsMasterKey); err != nil { + tagsInput := toTagsInput(bucketTags) + err := archival.UpdateCloudNativeStorageSetting[aws.StorageSettingUpdateResult](ctx, a.client, targetMappingID, aws.StorageSettingUpdateParams{ + Name: name, + StorageClass: storageClass, + KmsMasterKey: kmsMasterKey, + DeleteAllBucketTags: tagsInput == nil, + BucketTags: tagsInput, + }) + if err != nil { return fmt.Errorf("failed to update cloud native storage setting: %s", err) } return nil } -// toTargetMapping converts an aws.TargetMapping to a TargetMapping. +func toTagsInput(bucketTags map[string]string) *aws.TagsInput { + if len(bucketTags) == 0 { + return nil + } + + tags := make([]aws.Tag, 0, len(bucketTags)) + for key, value := range bucketTags { + tags = append(tags, aws.Tag{Key: key, Value: value}) + } + + return &aws.TagsInput{TagList: tags} +} + func toTargetMapping(target aws.TargetMapping) TargetMapping { bucketTags := make(map[string]string, len(target.TargetTemplate.BucketTags)) for _, tag := range target.TargetTemplate.BucketTags { diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index ea6d34dc..0dad8d8a 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -236,6 +236,43 @@ func (a API) Account(ctx context.Context, id IdentityFunc, feature core.Feature) return CloudAccount{}, fmt.Errorf("account %w", graphql.ErrNotFound) } +// AccountByNativeID returns the account with the specified feature and native +// ID. +func (a API) AccountByNativeID(ctx context.Context, feature core.Feature, nativeID string) (CloudAccount, error) { + a.log.Print(log.Trace) + + accounts, err := a.Accounts(ctx, feature, nativeID) + if err != nil { + return CloudAccount{}, fmt.Errorf("failed to get account by native id: %s", err) + } + + for _, account := range accounts { + if account.NativeID == nativeID { + return account, nil + } + } + + return CloudAccount{}, fmt.Errorf("account %q %w", nativeID, graphql.ErrNotFound) +} + +// AccountByName returns the account with the specified feature and name. +func (a API) AccountByName(ctx context.Context, feature core.Feature, name string) (CloudAccount, error) { + a.log.Print(log.Trace) + + accounts, err := a.Accounts(ctx, feature, name) + if err != nil { + return CloudAccount{}, fmt.Errorf("failed to get account by name: %s", err) + } + + for _, account := range accounts { + if account.Name == name { + return account, nil + } + } + + return CloudAccount{}, fmt.Errorf("account %q %w", name, graphql.ErrNotFound) +} + // Accounts return all accounts with the specified feature matching the filter. // The filter can be used to search for account id, account name and role arn. func (a API) Accounts(ctx context.Context, feature core.Feature, filter string) ([]CloudAccount, error) { @@ -303,7 +340,7 @@ func (a API) AddAccount(ctx context.Context, account AccountFunc, features []cor return uuid.Nil, err } - // If the RSC cloud account did not exist prior we retrieve the RSC cloud + // If the RSC cloud account did not exist prior, we retrieve the RSC cloud // account id. if akkount.ID == uuid.Nil { akkount, err = a.Account(ctx, AccountID(config.id), core.FeatureAll) @@ -355,11 +392,10 @@ func (a API) addAccountWithCFT(ctx context.Context, features []core.Feature, con return nil } -// RemoveAccount removes the account with the specified id from RSC for the -// given feature. If the Cloud Native Protection feature is being removed and -// deleteSnapshots is true the snapshots are deleted otherwise they are kept. -// Note that removing the Cloud Native Protection feature will also remove the -// Exocompute feature. +// RemoveAccount removes the RSC feature from the account with the specified id. +// +// If a Cloud Native Protection feature is being removed and deleteSnapshots is +// true, the snapshots are deleted otherwise they are kept. func (a API) RemoveAccount(ctx context.Context, account AccountFunc, features []core.Feature, deleteSnapshots bool) error { a.log.Print(log.Trace) @@ -371,36 +407,39 @@ func (a API) RemoveAccount(ctx context.Context, account AccountFunc, features [] return fmt.Errorf("failed to lookup account: %s", err) } - akkount, err := a.Account(ctx, AccountID(config.id), core.FeatureAll) + cloudAccount, err := a.Account(ctx, AccountID(config.id), core.FeatureAll) if err != nil { return fmt.Errorf("failed to get account: %s", err) } // Check that the account has all the features that are going to be removed. for _, feature := range features { - if _, ok := akkount.Feature(feature); !ok { + if _, ok := cloudAccount.Feature(feature); !ok { return fmt.Errorf("feature %s %w", feature, graphql.ErrNotFound) } } if config.config != nil { for _, feature := range features { - if err := a.removeAccountWithCFT(ctx, config, akkount, feature, deleteSnapshots); err != nil { + if err := a.removeAccountWithCFT(ctx, config, cloudAccount, feature, deleteSnapshots); err != nil { return err } } return nil } - return a.removeAccount(ctx, akkount, features, deleteSnapshots) + return a.removeAccount(ctx, cloudAccount, features, deleteSnapshots) } func (a API) removeAccount(ctx context.Context, account CloudAccount, features []core.Feature, deleteSnapshots bool) error { a.log.Print(log.Trace) for _, feature := range features { + if feature.Equal(core.FeatureExocompute) { + continue + } if err := a.disableFeature(ctx, account, feature, deleteSnapshots); err != nil { - return fmt.Errorf("failed to disable native account: %s", err) + return fmt.Errorf("failed to disable feature %s: %s", feature, err) } } @@ -426,7 +465,7 @@ func (a API) removeAccountWithCFT(ctx context.Context, config account, account C a.log.Print(log.Trace) if err := a.disableFeature(ctx, account, feature, deleteSnapshots); err != nil { - return fmt.Errorf("failed to disable native account: %s", err) + return fmt.Errorf("failed to disable feature %s: %s", feature, err) } cfmURL, err := aws.Wrap(a.client).PrepareCloudAccountDeletion(ctx, account.ID, feature) @@ -479,65 +518,96 @@ func (a API) removeAccountWithCFT(ctx context.Context, config account, account C return nil } +// disableFeature disables the specified account feature. func (a API) disableFeature(ctx context.Context, account CloudAccount, feature core.Feature, deleteSnapshots bool) error { a.log.Print(log.Trace) - rmFeature, _ := account.Feature(feature) - if !featureNeedsToBeDisable(rmFeature) { + // If the feature has not been onboarded or the feature is in the disabled + // or connecting state, there is no need to disable the feature. + if feature, ok := account.Feature(feature); ok { + if feature.Status == core.StatusDisabled || feature.Status == core.StatusConnecting { + return nil + } + } else { return nil } + // Only the following features need to be disabled. Note that the Exocompute + // feature only needs to be disabled in the CFT workflow. switch { - case rmFeature.Equal(core.FeatureCloudNativeProtection): - return a.disableNativeAccount(ctx, account.ID, aws.EC2, deleteSnapshots) - - case rmFeature.Equal(core.FeatureRDSProtection): - return a.disableNativeAccount(ctx, account.ID, aws.RDS, deleteSnapshots) - - case rmFeature.Equal(core.FeatureExocompute): + case feature.Equal(core.FeatureCloudNativeProtection): + return a.disableProtectionFeature(ctx, account.ID, aws.EC2, deleteSnapshots) + case feature.Equal(core.FeatureRDSProtection): + return a.disableProtectionFeature(ctx, account.ID, aws.RDS, deleteSnapshots) + case feature.Equal(core.FeatureCloudNativeS3Protection): + return a.disableProtectionFeature(ctx, account.ID, aws.S3, deleteSnapshots) + case feature.Equal(core.FeatureExocompute): jobID, err := aws.Wrap(a.client).StartExocomputeDisableJob(ctx, account.ID) if err != nil { - return fmt.Errorf("failed to disable native account: %s", err) + return fmt.Errorf("failed to disable exocompute feature: %s", err) } - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) - if err != nil { - return fmt.Errorf("failed to wait for taskchain: %s", err) - } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) + if err := core.Wrap(a.client).WaitForFeatureDisableTaskChain(ctx, jobID, func(ctx context.Context) (bool, error) { + account, err := a.Account(ctx, CloudAccountID(account.ID), feature) + if err != nil { + return false, fmt.Errorf("failed to retrieve status for feature %s: %s", feature, err) + } + + feature, ok := account.Feature(feature) + if !ok { + return false, fmt.Errorf("failed to retrieve status for feature %s: not found", feature) + } + return feature.Status == core.StatusDisabled, nil + }); err != nil { + return fmt.Errorf("failed to wait for task chain %s: %s", jobID, err) } } return nil } -// featureNeedsToBeDisable returns true if the specified feature needs to be -// disabled before being removed. Note, a feature in the connecting state can be -// removed without being disabling first. -func featureNeedsToBeDisable(feature Feature) bool { - return feature.Status != core.StatusDisabled && feature.Status != core.StatusConnecting -} +// disableProtectionFeature disables the specific Protection Feature of the +// cloud native protection feature. +func (a API) disableProtectionFeature(ctx context.Context, cloudAccountID uuid.UUID, protectionFeature aws.ProtectionFeature, deleteSnapshots bool) error { + a.log.Print(log.Trace) -func (a API) disableNativeAccount(ctx context.Context, id uuid.UUID, protectionFeature aws.ProtectionFeature, deleteSnapshots bool) error { - jobID, err := aws.Wrap(a.client).StartNativeAccountDisableJob(ctx, id, protectionFeature, deleteSnapshots) - if err != nil { - return fmt.Errorf("failed to disable native account: %s", err) + var feature core.Feature + switch protectionFeature { + case aws.EC2: + feature = core.FeatureCloudNativeProtection + case aws.RDS: + feature = core.FeatureRDSProtection + case aws.S3: + feature = core.FeatureCloudNativeS3Protection + default: + return fmt.Errorf("invalid protection feature: %s", protectionFeature) } - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) + jobID, err := aws.Wrap(a.client).StartNativeAccountDisableJob(ctx, cloudAccountID, protectionFeature, deleteSnapshots) if err != nil { - return fmt.Errorf("failed to wait for task chain: %s", err) + return fmt.Errorf("failed to disable protection feature %s: %s", protectionFeature, err) } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) + + if err := core.Wrap(a.client).WaitForFeatureDisableTaskChain(ctx, jobID, func(ctx context.Context) (bool, error) { + account, err := a.Account(ctx, CloudAccountID(cloudAccountID), feature) + if err != nil { + return false, fmt.Errorf("failed to retrieve status for feature %s: %s", feature, err) + } + + feature, ok := account.Feature(feature) + if !ok { + return false, fmt.Errorf("failed to retrieve status for feature %s: not found", feature) + } + return feature.Status == core.StatusDisabled, nil + }); err != nil { + return fmt.Errorf("failed to wait for task chain %s: %s", jobID, err) } return nil } // UpdateAccount updates the account with the specified id and feature. Note -// that account name is not tied to a specific feature. +// that the account name is not tied to a specific feature. func (a API) UpdateAccount(ctx context.Context, id IdentityFunc, feature core.Feature, opts ...OptionFunc) error { a.log.Print(log.Trace) diff --git a/pkg/polaris/aws/exocompute.go b/pkg/polaris/aws/exocompute.go index 53ecf80d..f7bd6b36 100644 --- a/pkg/polaris/aws/exocompute.go +++ b/pkg/polaris/aws/exocompute.go @@ -26,9 +26,9 @@ import ( "fmt" "github.com/google/uuid" - "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/exocompute" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) @@ -38,6 +38,7 @@ type Subnet struct { AvailabilityZone string } +// HealthCheckStatus represents the health status of an exocompute cluster. type HealthCheckStatus struct { Status string FailureReason string @@ -63,9 +64,9 @@ type ExocomputeConfig struct { HealthCheckStatus HealthCheckStatus } -// ExoConfigFunc returns an exocompute config initialized from the values -// passed to the function creating the ExoConfigFunc. -type ExoConfigFunc func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExocomputeConfigCreate, error) +// ExoConfigFunc returns an ExoCreateParams object initialized from the values +// passed to the function when creating the ExoConfigFunc. +type ExoConfigFunc func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExoCreateParams, error) // hasSecurityGroup returns true if a security group with the specified id // exists. @@ -104,39 +105,36 @@ func findSubnet(vpc aws.VPC, subnetID string) (aws.Subnet, error) { return aws.Subnet{}, fmt.Errorf("invalid subnet id: %v", subnetID) } -// Managed returns an ExoConfigFunc that initializes an exocompute config with -// security groups managed by RSC using the specified values. +// Managed returns an ExoConfigFunc that initializes an ExoCreateParams object +// with security groups managed by RSC using the specified values. func Managed(region, vpcID string, subnetIDs []string) ExoConfigFunc { - return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExocomputeConfigCreate, error) { - reg, err := aws.ParseRegion(region) - if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to parse region: %v", err) - } + return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExoCreateParams, error) { + reg := aws.ParseRegionNoValidation(region) // Validate VPC. vpcs, err := aws.Wrap(gql).AllVpcsByRegion(ctx, id, reg) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to get vpcs: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to get vpcs: %v", err) } vpc, err := findVPC(vpcs, vpcID) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find vpc: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find vpc: %v", err) } // Validate subnets. if len(subnetIDs) != 2 { - return aws.ExocomputeConfigCreate{}, errors.New("there should be exactly 2 subnet ids") + return aws.ExoCreateParams{}, errors.New("there should be exactly 2 subnet ids") } subnet1, err := findSubnet(vpc, subnetIDs[0]) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find subnet: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find subnet: %v", err) } subnet2, err := findSubnet(vpc, subnetIDs[1]) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find subnet: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find subnet: %v", err) } - return aws.ExocomputeConfigCreate{ + return aws.ExoCreateParams{ Region: reg, VPCID: vpcID, Subnets: []aws.Subnet{subnet1, subnet2}, @@ -145,49 +143,46 @@ func Managed(region, vpcID string, subnetIDs []string) ExoConfigFunc { } } -// Unmanaged returns an ExoConfigFunc that initializes an exocompute config +// Unmanaged returns an ExoConfigFunc that initializes an ExoCreateParams object // with security groups managed by the user using the specified values. func Unmanaged(region, vpcID string, subnetIDs []string, clusterSecurityGroupID, nodeSecurityGroupID string) ExoConfigFunc { - return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExocomputeConfigCreate, error) { - reg, err := aws.ParseRegion(region) - if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to parse region: %v", err) - } + return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExoCreateParams, error) { + reg := aws.ParseRegionNoValidation(region) // Validate VPC. vpcs, err := aws.Wrap(gql).AllVpcsByRegion(ctx, id, reg) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to get vpcs: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to get vpcs: %v", err) } vpc, err := findVPC(vpcs, vpcID) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find vpc: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find vpc: %v", err) } // Validate subnets. if len(subnetIDs) != 2 { - return aws.ExocomputeConfigCreate{}, errors.New("there should be exactly 2 subnet ids") + return aws.ExoCreateParams{}, errors.New("there should be exactly 2 subnet ids") } subnet1, err := findSubnet(vpc, subnetIDs[0]) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find subnet: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find subnet: %v", err) } subnet2, err := findSubnet(vpc, subnetIDs[1]) if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to find subnet: %v", err) + return aws.ExoCreateParams{}, fmt.Errorf("failed to find subnet: %v", err) } // Validate security groups. if !hasSecurityGroup(vpc, clusterSecurityGroupID) { - return aws.ExocomputeConfigCreate{}, + return aws.ExoCreateParams{}, fmt.Errorf("invalid cluster security group id: %v", clusterSecurityGroupID) } if !hasSecurityGroup(vpc, nodeSecurityGroupID) { - return aws.ExocomputeConfigCreate{}, + return aws.ExoCreateParams{}, fmt.Errorf("invalid node security group id: %v", nodeSecurityGroupID) } - return aws.ExocomputeConfigCreate{ + return aws.ExoCreateParams{ Region: reg, VPCID: vpcID, Subnets: []aws.Subnet{subnet1, subnet2}, @@ -201,19 +196,14 @@ func Unmanaged(region, vpcID string, subnetIDs []string, clusterSecurityGroupID, // BYOKCluster returns an ExoConfigFunc that initializes an exocompute config // with a Bring-Your-Own-Kubernetes cluster. func BYOKCluster(region string) ExoConfigFunc { - return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExocomputeConfigCreate, error) { - reg, err := aws.ParseRegion(region) - if err != nil { - return aws.ExocomputeConfigCreate{}, fmt.Errorf("failed to parse region: %v", err) - } - - return aws.ExocomputeConfigCreate{Region: reg}, nil + return func(ctx context.Context, gql *graphql.Client, id uuid.UUID) (aws.ExoCreateParams, error) { + return aws.ExoCreateParams{Region: aws.ParseRegionNoValidation(region)}, nil } } // toExocomputeConfig converts a polaris/graphql/aws exocompute config to a // polaris/aws exocompute config. -func toExocomputeConfig(config aws.ExocomputeConfig) (ExocomputeConfig, error) { +func toExocomputeConfig(config aws.ExoConfig) (ExocomputeConfig, error) { id, err := uuid.Parse(config.ID) if err != nil { return ExocomputeConfig{}, fmt.Errorf("invalid exocompute configuration id: %s", err) @@ -248,7 +238,7 @@ func toExocomputeConfig(config aws.ExocomputeConfig) (ExocomputeConfig, error) { func (a API) ExocomputeConfig(ctx context.Context, configID uuid.UUID) (ExocomputeConfig, error) { a.log.Print(log.Trace) - configsForAccounts, err := aws.Wrap(a.client).ExocomputeConfigs(ctx, "") + configsForAccounts, err := exocompute.ListConfigurations[aws.ExoConfigsForAccount](ctx, a.client, "") if err != nil { return ExocomputeConfig{}, fmt.Errorf("failed to get exocompute configs for account: %s", err) } @@ -275,7 +265,7 @@ func (a API) ExocomputeConfigs(ctx context.Context, id IdentityFunc) ([]Exocompu return nil, fmt.Errorf("failed to get native id: %s", err) } - configsForAccounts, err := aws.Wrap(a.client).ExocomputeConfigs(ctx, nativeID) + configsForAccounts, err := exocompute.ListConfigurations[aws.ExoConfigsForAccount](ctx, a.client, nativeID) if err != nil { return nil, fmt.Errorf("failed to get exocompute configs for account: %s", err) } @@ -309,16 +299,11 @@ func (a API) AddExocomputeConfig(ctx context.Context, id IdentityFunc, config Ex return uuid.Nil, fmt.Errorf("failed to lookup exocompute config: %s", err) } - exo, err := aws.Wrap(a.client).CreateExocomputeConfig(ctx, accountID, exoConfig) + exoID, err := exocompute.CreateConfiguration[aws.ExoCreateResult](ctx, a.client, accountID, exoConfig) if err != nil { return uuid.Nil, fmt.Errorf("failed to create exocompute config: %s", err) } - exoID, err := uuid.Parse(exo.ID) - if err != nil { - return uuid.Nil, fmt.Errorf("invalid exocompute configuration id: %s", err) - } - return exoID, nil } @@ -335,16 +320,11 @@ func (a API) UpdateExocomputeConfig(ctx context.Context, id IdentityFunc, config return uuid.Nil, fmt.Errorf("failed to lookup exocompute config: %s", err) } - exo, err := aws.Wrap(a.client).UpdateExocomputeConfig(ctx, accountID, exoConfig) + exoID, err := exocompute.UpdateConfiguration[aws.ExoUpdateResult](ctx, a.client, accountID, aws.ExoUpdateParams(exoConfig)) if err != nil { return uuid.Nil, fmt.Errorf("failed to create exocompute config: %s", err) } - exoID, err := uuid.Parse(exo.ID) - if err != nil { - return uuid.Nil, fmt.Errorf("invalid exocompute configuration id: %s", err) - } - return exoID, nil } @@ -353,16 +333,16 @@ func (a API) UpdateExocomputeConfig(ctx context.Context, id IdentityFunc, config func (a API) RemoveExocomputeConfig(ctx context.Context, configID uuid.UUID) error { a.log.Print(log.Trace) - err := aws.Wrap(a.client).DeleteExocomputeConfig(ctx, configID) + err := exocompute.DeleteConfiguration[aws.ExoDeleteResult](ctx, a.client, configID) if err != nil { - return fmt.Errorf("failed to remove exocompute config: %v", err) + return fmt.Errorf("failed to remove exocompute config: %s", err) } return nil } -// ExocomputeHostAccount returns the exocompute host account for the exocompute -// application account with the specified id. +// ExocomputeHostAccount returns the exocompute host cloud account ID for the +// specified application cloud account. func (a API) ExocomputeHostAccount(ctx context.Context, appID IdentityFunc) (uuid.UUID, error) { a.log.Print(log.Trace) @@ -371,7 +351,7 @@ func (a API) ExocomputeHostAccount(ctx context.Context, appID IdentityFunc) (uui return uuid.Nil, fmt.Errorf("failed to get cloud account id: %s", err) } - configsForAccounts, err := aws.Wrap(a.client).ExocomputeConfigs(ctx, "") + configsForAccounts, err := exocompute.ListConfigurations[aws.ExoConfigsForAccount](ctx, a.client, "") if err != nil { return uuid.Nil, fmt.Errorf("failed to get exocompute configs for account: %s", err) } @@ -387,38 +367,8 @@ func (a API) ExocomputeHostAccount(ctx context.Context, appID IdentityFunc) (uui return uuid.Nil, fmt.Errorf("exocompute account %w", graphql.ErrNotFound) } -// ExocomputeApplicationAccounts returns the exocompute application accounts for -// the exocompute host account with the specified id. -func (a API) ExocomputeApplicationAccounts(ctx context.Context, hostID IdentityFunc) ([]uuid.UUID, error) { - a.log.Print(log.Trace) - - nativeID, err := a.toNativeID(ctx, hostID) - if err != nil { - return nil, fmt.Errorf("failed to get native id: %s", err) - } - - configsForAccounts, err := aws.Wrap(a.client).ExocomputeConfigs(ctx, nativeID) - if err != nil { - return nil, fmt.Errorf("failed to get exocompute configs for account: %s", err) - } - - var mappedAccounts []uuid.UUID - for _, configsForAccount := range configsForAccounts { - if configsForAccount.Account.NativeID == nativeID { - for _, mappedAccount := range configsForAccount.MappedAccounts { - mappedAccounts = append(mappedAccounts, mappedAccount.ID) - } - } - } - if len(mappedAccounts) == 0 { - return nil, fmt.Errorf("exocompute mapped accounts %w", graphql.ErrNotFound) - } - - return mappedAccounts, nil -} - -// MapExocompute maps the exocompute application account to the specified -// exocompute host account. +// MapExocompute maps the exocompute application cloud account to the specified +// host cloud account. func (a API) MapExocompute(ctx context.Context, hostID IdentityFunc, appID IdentityFunc) error { a.log.Print(log.Trace) @@ -432,41 +382,43 @@ func (a API) MapExocompute(ctx context.Context, hostID IdentityFunc, appID Ident return fmt.Errorf("failed to get cloud account id: %s", err) } - if err = aws.Wrap(a.client).MapCloudAccountExocomputeAccount(ctx, hostCloudAccountID, []uuid.UUID{appCloudAccountID}); err != nil { - return fmt.Errorf("failed to map exocompute config: %v", err) + if err := exocompute.MapCloudAccount[aws.ExoMapResult](ctx, a.client, hostCloudAccountID, appCloudAccountID); err != nil { + return fmt.Errorf("failed to map exocompute config: %s", err) } return nil } -// UnmapExocompute unmaps the exocompute application account with the specified -// id. +// UnmapExocompute unmaps the exocompute application cloud account with the +// specified cloud account ID. func (a API) UnmapExocompute(ctx context.Context, appID IdentityFunc) error { a.log.Print(log.Trace) - cloudAccountID, err := a.toCloudAccountID(ctx, appID) + appCloudAccountID, err := a.toCloudAccountID(ctx, appID) if err != nil { return fmt.Errorf("failed to get cloud account id: %s", err) } - if err := aws.Wrap(a.client).UnmapCloudAccountExocomputeAccount(ctx, []uuid.UUID{cloudAccountID}); err != nil { - return fmt.Errorf("failed to unmap exocompute config: %v", err) + if err := exocompute.UnmapCloudAccount[aws.ExoUnmapResult](ctx, a.client, appCloudAccountID); err != nil { + return fmt.Errorf("failed to unmap exocompute config: %s", err) } return nil } // AddClusterToExocomputeConfig adds the named cluster to specified exocompute -// configration. The cluster ID and connection command are returned. -func (a API) AddClusterToExocomputeConfig(ctx context.Context, configID uuid.UUID, clusterName string) (uuid.UUID, string, error) { +// configuration. The cluster ID and two different ways to connect the cluster +// are returned. The first way to connect the cluster is the kubectl connection +// command, and the second way is the k8s spec (YAML). +func (a API) AddClusterToExocomputeConfig(ctx context.Context, configID uuid.UUID, clusterName string) (uuid.UUID, string, string, error) { a.log.Print(log.Trace) - clusterID, cmd, err := aws.Wrap(a.client).ConnectExocomputeCluster(ctx, configID, clusterName) + clusterID, kubectlCmd, setupYAML, err := aws.Wrap(a.client).ConnectExocomputeCluster(ctx, configID, clusterName) if err != nil { - return uuid.Nil, "", fmt.Errorf("failed to connect exocompute cluster: %v", err) + return uuid.Nil, "", "", fmt.Errorf("failed to connect exocompute cluster: %s", err) } - return clusterID, cmd, nil + return clusterID, kubectlCmd, setupYAML, nil } // RemoveExocomputeCluster removes the exocompute cluster with the specified ID. @@ -474,7 +426,7 @@ func (a API) RemoveExocomputeCluster(ctx context.Context, clusterID uuid.UUID) e a.log.Print(log.Trace) if err := aws.Wrap(a.client).DisconnectExocomputeCluster(ctx, clusterID); err != nil { - return fmt.Errorf("failed to disconnect exocompute cluster: %v", err) + return fmt.Errorf("failed to disconnect exocompute cluster: %s", err) } return nil diff --git a/pkg/polaris/aws/options.go b/pkg/polaris/aws/options.go index f8722b17..105d5fc0 100644 --- a/pkg/polaris/aws/options.go +++ b/pkg/polaris/aws/options.go @@ -22,7 +22,6 @@ package aws import ( "context" - "fmt" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" ) @@ -49,17 +48,12 @@ func Name(name string) OptionFunc { // instance. func Region(region string) OptionFunc { return func(ctx context.Context, opts *options) error { - r, err := aws.ParseRegion(region) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } - - opts.regions = append(opts.regions, r) + opts.regions = append(opts.regions, aws.ParseRegionNoValidation(region)) return nil } } -// Regions returns an OptionFunc that gives the specified regions to the +// Regions return an OptionFunc that gives the specified regions to the // options instance. func Regions(regions ...string) OptionFunc { return func(ctx context.Context, opts *options) error { @@ -69,11 +63,7 @@ func Regions(regions ...string) OptionFunc { } for _, r := range regions { - region, err := aws.ParseRegion(r) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } - + region := aws.ParseRegionNoValidation(r) if _, ok := set[region]; !ok { opts.regions = append(opts.regions, region) set[region] = struct{}{} diff --git a/pkg/polaris/azure/archival.go b/pkg/polaris/azure/archival.go new file mode 100644 index 00000000..9db137c0 --- /dev/null +++ b/pkg/polaris/azure/archival.go @@ -0,0 +1,271 @@ +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package azure + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/archival" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" +) + +// TargetMapping represents an Azure cloud archival location. +type TargetMapping struct { + ID uuid.UUID + Name string + ArchivalGroup string + ArchivalTarget string + ConnectionStatus string + ContainerName string + StorageAccountName string + StorageAccountRegion string + StorageAccountTags map[string]string + LocTemplate string + Redundancy string + StorageTier string + NativeID uuid.UUID + CustomerKeys []CustomerKey +} + +// CustomerKey represents the customer managed key information required for +// encryption of Azure storage. +type CustomerKey struct { + Name string + Region string + VaultName string +} + +// TargetMappingByID returns the Azure target mapping with the specified ID. +// If no target mapping with the specified ID is found, graphql.ErrNotFound is +// returned. +func (a API) TargetMappingByID(ctx context.Context, id uuid.UUID) (TargetMapping, error) { + a.log.Print(log.Trace) + + filter := []azure.TargetMappingFilter{{ + Field: "ARCHIVAL_GROUP_ID", + Text: id.String(), + }} + targets, err := archival.ListTargetMappings[azure.TargetMapping](ctx, a.client, filter) + if err != nil { + return TargetMapping{}, fmt.Errorf("failed to get target mappings: %s", err) + } + + for _, target := range targets { + if target.ID == id { + return toTargetMapping(target), nil + } + } + + return TargetMapping{}, fmt.Errorf("target mapping for %q %w", id, graphql.ErrNotFound) +} + +// TargetMappingByName returns the Azure target mapping with the specified name. +// If no target mapping with the specified ID is found, graphql.ErrNotFound is +// returned. +func (a API) TargetMappingByName(ctx context.Context, name string) (TargetMapping, error) { + a.log.Print(log.Trace) + + filter := []azure.TargetMappingFilter{{ + Field: "NAME", + Text: name, + }} + targets, err := archival.ListTargetMappings[azure.TargetMapping](ctx, a.client, filter) + if err != nil { + return TargetMapping{}, fmt.Errorf("failed to get target mappings: %s", err) + } + + for _, target := range targets { + if target.Name == name { + return toTargetMapping(target), nil + } + } + + return TargetMapping{}, fmt.Errorf("target mapping for %q %w", name, graphql.ErrNotFound) +} + +// TargetMappings returns all Azure target mappings that match the specified +// archival group and name filter. The name filter can be used to search for +// prefixes of a name. If the name filter is empty, is will match all names. +// In RSC cloud, archival locations are also referred to as target mappings. +func (a API) TargetMappings(ctx context.Context, nameFilter string) ([]TargetMapping, error) { + a.log.Print(log.Trace) + + filter := []azure.TargetMappingFilter{{ + Field: "NAME", + Text: nameFilter, + }} + targets, err := archival.ListTargetMappings[azure.TargetMapping](ctx, a.client, filter) + if err != nil { + return nil, fmt.Errorf("failed to get target mappings: %s", err) + } + + targetMappings := make([]TargetMapping, 0, len(targets)) + for _, target := range targets { + targetMappings = append(targetMappings, toTargetMapping(target)) + } + + return targetMappings, nil +} + +// DeleteTargetMapping deletes the target mapping with the specified ID. In RSC +// cloud archival locations are also referred to as target mappings. +func (a API) DeleteTargetMapping(ctx context.Context, id uuid.UUID) error { + a.log.Print(log.Trace) + + return archival.DeleteTargetMapping(ctx, a.client, id) +} + +// CreateStorageSetting creates a cloud native archival location. The storage +// account region, the storage account tags, and the customer managed keys are +// optional. +func (a API) CreateStorageSetting(ctx context.Context, id IdentityFunc, name, redundancy, storageTier, storageAccountName, storageAccountRegion string, storageAccountTags map[string]string, customerKeys []CustomerKey) (uuid.UUID, error) { + a.log.Print(log.Trace) + + cloudAccount, err := a.Subscription(ctx, id, core.FeatureAll) + if err != nil { + return uuid.Nil, err + } + + var storageAccountRegionEnum *azure.RegionEnum + saRegion := azure.RegionFromName(storageAccountRegion) + if saRegion != azure.RegionUnknown { + regionEnum := saRegion.ToRegionEnum() + storageAccountRegionEnum = ®ionEnum + } + locTemplate := "SPECIFIC_REGION" + if storageAccountRegionEnum == nil { + locTemplate = "SOURCE_REGION" + } + + var tags *struct { + TagList []azure.Tag `json:"tagList"` + } + if len(storageAccountTags) > 0 { + tags = &struct { + TagList []azure.Tag `json:"tagList"` + }{TagList: make([]azure.Tag, 0, len(storageAccountTags))} + for key, value := range storageAccountTags { + tags.TagList = append(tags.TagList, azure.Tag{Key: key, Value: value}) + } + } + + keys := make([]azure.CustomerKey, 0, len(customerKeys)) + for _, key := range customerKeys { + keys = append(keys, azure.CustomerKey{ + KeyName: key.Name, + KeyVaultName: key.VaultName, + Region: azure.RegionFromName(key.Region).ToRegionEnum(), + }) + } + + targetMappingID, err := archival.CreateCloudNativeStorageSetting[azure.StorageSettingCreateResult](ctx, a.client, + cloudAccount.ID, azure.StorageSettingCreateParams{ + LocTemplate: locTemplate, + Name: name, + Redundancy: redundancy, + StorageTier: storageTier, + NativeID: cloudAccount.NativeID, + StorageAccountName: storageAccountName, + StorageAccountRegion: storageAccountRegionEnum, + StorageAccountTags: tags, + CMKInfo: keys, + }) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to create cloud native storage setting: %s", err) + } + + return targetMappingID, nil +} + +// UpdateStorageSetting updates the cloud native archival location with the +// specified ID. Note that not all properties can be updated, only the name, +// storage tier, storage account tags and customer managed keys. +func (a API) UpdateStorageSetting(ctx context.Context, targetMappingID uuid.UUID, name, storageTier string, storageAccountTags map[string]string, customerKeys []CustomerKey) error { + a.log.Print(log.Trace) + + tags := make([]azure.Tag, 0, len(storageAccountTags)) + for key, value := range storageAccountTags { + tags = append(tags, azure.Tag{Key: key, Value: value}) + } + + keys := make([]azure.CustomerKey, 0, len(customerKeys)) + for _, key := range customerKeys { + keys = append(keys, azure.CustomerKey{ + KeyName: key.Name, + KeyVaultName: key.VaultName, + Region: azure.RegionFromName(key.Region).ToRegionEnum(), + }) + } + + err := archival.UpdateCloudNativeStorageSetting[azure.StorageSettingUpdateResult](ctx, a.client, targetMappingID, + azure.StorageSettingUpdateParams{ + Name: name, + StorageTier: storageTier, + StorageAccountTags: struct { + TagList []azure.Tag `json:"tagList"` + }{TagList: tags}, + CMKInfo: keys, + }) + if err != nil { + return fmt.Errorf("failed to update cloud native storage setting: %s", err) + } + + return nil +} + +// toTargetMapping converts an aws.TargetMapping to a TargetMapping. +func toTargetMapping(target azure.TargetMapping) TargetMapping { + tags := make(map[string]string, len(target.TargetTemplate.CloudNativeCompanion.StorageAccountTags)) + for _, tag := range target.TargetTemplate.CloudNativeCompanion.StorageAccountTags { + tags[tag.Key] = tag.Value + } + + keys := make([]CustomerKey, 0, len(target.TargetTemplate.CloudNativeCompanion.CMKInfo)) + for _, key := range target.TargetTemplate.CloudNativeCompanion.CMKInfo { + keys = append(keys, CustomerKey{ + Name: key.KeyName, + Region: key.Region.Name(), + VaultName: key.KeyVaultName, + }) + } + + return TargetMapping{ + ID: target.ID, + Name: target.Name, + ArchivalGroup: target.GroupType, + ArchivalTarget: target.TargetType, + ConnectionStatus: target.ConnectionStatus.Status, + ContainerName: target.TargetTemplate.ContainerNamePrefix, + StorageAccountName: target.TargetTemplate.StorageAccountName, + StorageAccountRegion: target.TargetTemplate.CloudNativeCompanion.StorageAccountRegion.Name(), + StorageAccountTags: tags, + LocTemplate: target.TargetTemplate.CloudNativeCompanion.LocTemplate, + Redundancy: target.TargetTemplate.CloudNativeCompanion.Redundancy, + StorageTier: target.TargetTemplate.CloudNativeCompanion.StorageTier, + NativeID: target.TargetTemplate.CloudNativeCompanion.NativeID, + CustomerKeys: keys, + } +} diff --git a/pkg/polaris/azure/azure.go b/pkg/polaris/azure/azure.go index c6c77ed0..d1918752 100644 --- a/pkg/polaris/azure/azure.go +++ b/pkg/polaris/azure/azure.go @@ -18,15 +18,17 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package azure provides a high level interface to the Azure part of the RSC +// Package azure provides a high-level interface to the Azure part of the RSC // platform. package azure import ( + "cmp" "context" "errors" "fmt" - "time" + "slices" + "strings" "github.com/google/uuid" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" @@ -53,12 +55,23 @@ func Wrap(client *polaris.Client) API { return API{client: client.GQL, log: client.GQL.Log()} } -// CloudAccount for Microsoft Azure subscriptions. +// CloudAccountTenant represents an Azure tenant in RSC. +type CloudAccountTenant struct { + Cloud string + ID uuid.UUID // Rubrik tenant ID. + ClientID uuid.UUID // Azure app registration application id. + AppName string + DomainName string // Azure tenant domain. + SubscriptionCount int +} + +// CloudAccount for Azure subscriptions. type CloudAccount struct { - ID uuid.UUID - NativeID uuid.UUID + ID uuid.UUID // Rubrik cloud account ID. + NativeID uuid.UUID // Azure subscription ID. Name string - TenantDomain string + TenantID uuid.UUID // Rubrik tenant ID. + TenantDomain string // Azure tenant domain. Features []Feature } @@ -73,11 +86,13 @@ func (c CloudAccount) Feature(feature core.Feature) (Feature, bool) { return Feature{}, false } -// Feature for Microsoft Azure subscriptions. +// Feature for Azure cloud account. type Feature struct { core.Feature - Regions []string - Status core.Status + ResourceGroup FeatureResourceGroup + Regions []string + Status core.Status + UserAssignedManagedIdentity FeatureUserAssignedManagedIdentity } // HasRegion returns true if the feature is enabled for the specified region. @@ -91,17 +106,33 @@ func (f Feature) HasRegion(region string) bool { return false } -// RSC does not support the AllFeatures for Azure cloud accounts. We work around -// this by translating FeatureAll to the following list of features. -var allFeatures = []core.Feature{ - core.FeatureCloudNativeArchival, - core.FeatureCloudNativeArchivalEncryption, - core.FeatureCloudNativeProtection, - core.FeatureExocompute, +// SupportResourceGroup returns true if the feature supports being onboarded +// with a resource group. +func (f Feature) SupportResourceGroup() bool { + return !f.Equal(core.FeatureAzureSQLDBProtection) && !f.Equal(core.FeatureAzureSQLMIProtection) } -// toCloudAccountID returns the RSC cloud account id for the specified identity. -// If the identity is a RSC cloud account id no remote endpoint is called. +// SupportUserAssignedManagedIdentity returns true if the feature supports +// being onboarded with user-assigned managed identity. +func (f Feature) SupportUserAssignedManagedIdentity() bool { + return f.Equal(core.FeatureCloudNativeArchivalEncryption) +} + +type FeatureResourceGroup struct { + Name string + NativeID string + Tags map[string]string + Region string +} + +type FeatureUserAssignedManagedIdentity struct { + Name string + NativeID string + PrincipalID string +} + +// toCloudAccountID returns the RSC cloud account ID for the specified identity. +// If the identity is an RSC cloud account ID, no remote endpoint is called. func (a API) toCloudAccountID(ctx context.Context, id IdentityFunc) (uuid.UUID, error) { a.log.Print(log.Trace) @@ -121,39 +152,22 @@ func (a API) toCloudAccountID(ctx context.Context, id IdentityFunc) (uuid.UUID, return uid, nil } - // Note that the same tenant can show up for multiple features. - tenantIDs := make(map[uuid.UUID]struct{}) - for _, feature := range allFeatures { - tenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, feature, false) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to get tenants: %v", err) - } - for _, tenant := range tenants { - tenantIDs[tenant.ID] = struct{}{} - } + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, core.FeatureAll, true) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get tenants: %s", err) } - for tenantID := range tenantIDs { - for _, feature := range allFeatures { - tenantWithAccounts, err := azure.Wrap(a.client).CloudAccountTenant(ctx, tenantID, feature, identity.id) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to get tenant: %v", err) - } - - // Find the exact match. - for _, account := range tenantWithAccounts.Accounts { - if account.NativeID == uid { - return account.ID, nil - } - } + for _, account := range toSubscriptions(rawTenants) { + if account.NativeID == uid { + return account.ID, nil } } return uuid.Nil, fmt.Errorf("subscription %w", graphql.ErrNotFound) } -// toNativeID returns the Azure subscription id for the specified identity. -// If the identity is an Azure subscription id no remote endpoint is called. +// toNativeID returns the Azure subscription ID for the specified identity. +// If the identity is an Azure subscription ID, no remote endpoint is called. func (a API) toNativeID(ctx context.Context, id IdentityFunc) (uuid.UUID, error) { a.log.Print(log.Trace) @@ -169,109 +183,84 @@ func (a API) toNativeID(ctx context.Context, id IdentityFunc) (uuid.UUID, error) if err != nil { return uuid.Nil, fmt.Errorf("failed to parse identity: %v", err) } - if !identity.internal { return uid, nil } - // Note that the same tenant can show up for multiple features. - tenantIDs := make(map[uuid.UUID]struct{}) - for _, feature := range allFeatures { - tenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, feature, false) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to get tenants: %v", err) - } - for _, tenant := range tenants { - tenantIDs[tenant.ID] = struct{}{} - } + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, core.FeatureAll, true) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get tenants: %s", err) } - for tenantID := range tenantIDs { - for _, feature := range allFeatures { - tenantWithAccounts, err := azure.Wrap(a.client).CloudAccountTenant(ctx, tenantID, feature, "") - if err != nil { - return uuid.Nil, fmt.Errorf("failed to get tenant: %v", err) - } - for _, account := range tenantWithAccounts.Accounts { - if account.ID == uid { - return account.ID, nil - } - } + for _, account := range toSubscriptions(rawTenants) { + if account.ID == uid { + return account.NativeID, nil } } return uuid.Nil, fmt.Errorf("subscription %w", graphql.ErrNotFound) } -// subscriptions return all subscriptions for the given feature and filter. -func (a API) subscriptions(ctx context.Context, feature core.Feature, filter string) ([]CloudAccount, error) { +// Tenant returns the tenant with the specified ID. +func (a API) Tenant(ctx context.Context, tenantID uuid.UUID) (CloudAccountTenant, error) { a.log.Print(log.Trace) - tenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, feature, false) + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, core.FeatureAll, false) if err != nil { - return nil, fmt.Errorf("failed to get tenants: %v", err) + return CloudAccountTenant{}, fmt.Errorf("failed to get tenants: %s", err) } - var accounts []CloudAccount - for _, tenant := range tenants { - tenantWithAccounts, err := azure.Wrap(a.client).CloudAccountTenant(ctx, tenant.ID, feature, filter) - if err != nil { - return nil, fmt.Errorf("failed to get tenant: %v", err) - } - - for _, account := range tenantWithAccounts.Accounts { - accounts = append(accounts, CloudAccount{ - ID: account.ID, - NativeID: account.NativeID, - Name: account.Name, - TenantDomain: tenantWithAccounts.DomainName, - Features: []Feature{{ - Feature: core.Feature{Name: account.Feature.Feature}, - Regions: azure.FormatRegions(account.Feature.Regions), - Status: account.Feature.Status, - }}, - }) + for _, tenant := range toTenants(rawTenants) { + if tenant.ID == tenantID { + return tenant, nil } } - return accounts, nil + return CloudAccountTenant{}, fmt.Errorf("tenant %w", graphql.ErrNotFound) } -// subscriptionsAllFeatures return all subscriptions with all features for -// the given filter. Note that the organization name of the cloud account is -// not set. -func (a API) subscriptionsAllFeatures(ctx context.Context, filter string) ([]CloudAccount, error) { +// TenantFromAppID returns the tenant with the specified app registration +// application ID. +func (a API) TenantFromAppID(ctx context.Context, appID uuid.UUID) (CloudAccountTenant, error) { a.log.Print(log.Trace) - accountMap := make(map[uuid.UUID]*CloudAccount) - for _, feature := range allFeatures { - accounts, err := a.subscriptions(ctx, feature, filter) - if err != nil { - return nil, fmt.Errorf("failed to get subscriptions: %v", err) + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, core.FeatureAll, false) + if err != nil { + return CloudAccountTenant{}, fmt.Errorf("failed to get tenants: %s", err) + } + + for _, tenant := range toTenants(rawTenants) { + if tenant.ClientID == appID { + return tenant, nil } + } - for i := range accounts { - // We need to create a copy of the account here since we use it as a - // pointer further down. - account := accounts[i] + return CloudAccountTenant{}, fmt.Errorf("tenant %w", graphql.ErrNotFound) +} - if mapped, ok := accountMap[account.ID]; ok { - mapped.Features = append(mapped.Features, account.Features...) - } else { - accountMap[account.ID] = &account - } - } +// Tenants returns all tenants with the specified feature. This function accepts +// the FeatureAll feature. The filter can be used to search for application ID +// and tenant domain. +func (a API) Tenants(ctx context.Context, filter string) ([]CloudAccountTenant, error) { + a.log.Print(log.Trace) + + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, core.FeatureAll, false) + if err != nil { + return nil, fmt.Errorf("failed to get tenants: %s", err) } - accounts := make([]CloudAccount, 0, len(accountMap)) - for _, account := range accountMap { - accounts = append(accounts, *account) + // Filter tenants. + tenants := make([]CloudAccountTenant, 0, len(rawTenants)) + for _, tenant := range toTenants(rawTenants) { + if filter == "" || strings.HasPrefix(tenant.DomainName, filter) || strings.HasPrefix(tenant.ClientID.String(), filter) { + tenants = append(tenants, tenant) + } } - return accounts, nil + return tenants, nil } -// Subscription returns the subscription with specified id and feature. +// Subscription returns the subscription with specified ID and feature. func (a API) Subscription(ctx context.Context, id IdentityFunc, feature core.Feature) (CloudAccount, error) { a.log.Print(log.Trace) @@ -288,58 +277,97 @@ func (a API) Subscription(ctx context.Context, id IdentityFunc, feature core.Fea return CloudAccount{}, fmt.Errorf("failed to parse identity: %v", err) } - if identity.internal { - accounts, err := a.Subscriptions(ctx, feature, "") - if err != nil { - return CloudAccount{}, fmt.Errorf("failed to get subscriptions: %v", err) - } + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, feature, true) + if err != nil { + return CloudAccount{}, fmt.Errorf("failed to get tenants: %s", err) + } - // Find the exact match. - for _, account := range accounts { - if account.ID == uid { - return account, nil + // Find the exact match. + for _, subscription := range toSubscriptions(rawTenants) { + if identity.internal { + if subscription.ID == uid { + return subscription, nil + } + } else { + if subscription.NativeID == uid { + return subscription, nil } } - } else { - accounts, err := a.Subscriptions(ctx, feature, identity.id) - if err != nil { - return CloudAccount{}, fmt.Errorf("failed to get subscriptions: %v", err) + } + + return CloudAccount{}, fmt.Errorf("subscription %w", graphql.ErrNotFound) +} + +// SubscriptionByNativeID returns the subscription with the specified feature +// and native ID. +func (a API) SubscriptionByNativeID(ctx context.Context, feature core.Feature, nativeID uuid.UUID) (CloudAccount, error) { + a.log.Print(log.Trace) + + subscriptions, err := a.Subscriptions(ctx, feature, nativeID.String()) + if err != nil { + return CloudAccount{}, err + } + + for _, subscription := range subscriptions { + if subscription.NativeID == nativeID { + return subscription, nil } + } - // Find the exact match. - for _, account := range accounts { - if account.NativeID == uid { - return account, nil - } + return CloudAccount{}, fmt.Errorf("subscription %q %w", nativeID, graphql.ErrNotFound) +} + +// SubscriptionByName returns the subscription with the specified feature and +// name. Tenant domain is optional and ignored if an empty string is passed in. +func (a API) SubscriptionByName(ctx context.Context, feature core.Feature, name, tenantDomain string) (CloudAccount, error) { + a.log.Print(log.Trace) + + subscriptions, err := a.Subscriptions(ctx, feature, name) + if err != nil { + return CloudAccount{}, err + } + + // Sort the subscriptions ascending on tenant domain and name. + slices.SortFunc(subscriptions, func(i, j CloudAccount) int { + return cmp.Compare(i.TenantDomain+i.Name, j.TenantDomain+i.Name) + }) + for _, subscription := range subscriptions { + if subscription.Name == name && (tenantDomain == "" || subscription.TenantDomain == tenantDomain) { + return subscription, nil } } - return CloudAccount{}, fmt.Errorf("subscription %w", graphql.ErrNotFound) + if tenantDomain != "" { + name = tenantDomain + "/" + name + } + return CloudAccount{}, fmt.Errorf("subscription %q %w", name, graphql.ErrNotFound) } // Subscriptions return all subscriptions with the specified feature matching -// the filter. The filter can be used to search for subscription name and -// subscription id. +// the filter. The filter can be used to search for subscription name and native +// subscription ID. func (a API) Subscriptions(ctx context.Context, feature core.Feature, filter string) ([]CloudAccount, error) { a.log.Print(log.Trace) - var accounts []CloudAccount - var err error - if feature.Equal(core.FeatureAll) { - accounts, err = a.subscriptionsAllFeatures(ctx, filter) - } else { - accounts, err = a.subscriptions(ctx, feature, filter) - } + rawTenants, err := azure.Wrap(a.client).CloudAccountTenants(ctx, feature, true) if err != nil { - return nil, fmt.Errorf("failed to get subscriptions: %v", err) + return nil, fmt.Errorf("failed to get tenants: %s", err) + } + + // Filter subscriptions. + accounts := make([]CloudAccount, 0, len(rawTenants)) + for _, subscription := range toSubscriptions(rawTenants) { + if filter == "" || strings.HasPrefix(subscription.Name, filter) || strings.HasPrefix(subscription.NativeID.String(), filter) { + accounts = append(accounts, subscription) + } } return accounts, nil } -// AddSubscription adds the specified subscription to RSC. If name isn't given -// as an option it's derived from the tenant name. Returns the RSC cloud account -// id of the added subscription. +// AddSubscription adds the specified subscription to RSC. If a name isn't given +// as an option, it's derived from the tenant name. Returns the RSC cloud +// account ID of the added subscription. func (a API) AddSubscription(ctx context.Context, subscription SubscriptionFunc, feature core.Feature, opts ...OptionFunc) (uuid.UUID, error) { a.log.Print(log.Trace) @@ -357,16 +385,12 @@ func (a API) AddSubscription(ctx context.Context, subscription SubscriptionFunc, return uuid.Nil, fmt.Errorf("failed to lookup option: %v", err) } } - err = verifyOptionsForFeature(options, feature) - if err != nil { - return uuid.Nil, err - } if options.name != "" { config.name = options.name } - // If there already is an RSC cloud account for the given Azure subscription - // we use the same name when adding the new feature. + // If there already is an RSC cloud account for the given Azure + // subscription, we use the same name when adding the new feature. account, err := a.Subscription(ctx, SubscriptionID(config.id), core.FeatureAll) if err == nil { config.name = account.Name @@ -382,6 +406,7 @@ func (a API) AddSubscription(ctx context.Context, subscription SubscriptionFunc, cloudAccountFeature := azure.CloudAccountFeature{ PolicyVersion: perms.PermissionVersion, + PermissionGroups: perms.PermissionGroupVersions, FeatureType: feature.Name, ResourceGroup: options.resourceGroup, FeatureSpecificInfo: options.featureSpecificInfo, @@ -393,7 +418,7 @@ func (a API) AddSubscription(ctx context.Context, subscription SubscriptionFunc, return uuid.Nil, fmt.Errorf("failed to add subscription: %v", err) } - // If the RSC cloud account did not exist prior we retrieve the RSC cloud + // If the RSC cloud account did not exist prior, we retrieve the RSC cloud // account id. if account.ID == uuid.Nil { account, err = a.Subscription(ctx, SubscriptionID(config.id), feature) @@ -405,77 +430,75 @@ func (a API) AddSubscription(ctx context.Context, subscription SubscriptionFunc, return account.ID, nil } -// RemoveSubscription removes the subscription with the specified id from RSC. -// If deleteSnapshots is true the snapshots are deleted otherwise they are kept. +// RemoveSubscription removes the RSC feature from the subscription with the +// specified id. +// +// If a cloud native protection feature is being removed and deleteSnapshots is +// true, the snapshots are deleted otherwise they are kept. func (a API) RemoveSubscription(ctx context.Context, id IdentityFunc, feature core.Feature, deleteSnapshots bool) error { a.log.Print(log.Trace) account, err := a.Subscription(ctx, id, feature) if err != nil { - return fmt.Errorf("failed to get subscription: %w", err) + return fmt.Errorf("failed to retrieve subscription: %w", err) } - switch { - case account.Features[0].Equal(core.FeatureCloudNativeProtection) && account.Features[0].Status != core.StatusDisabled: - // Lookup the RSC native account id from the RSC subscription name and - // the Azure subscription id. The RSC native account id is needed to - // delete the RSC native account subscription. - nativeSubscriptions, err := azure.Wrap(a.client).NativeSubscriptions(ctx, account.Name) - if err != nil { - return fmt.Errorf("failed to get native subscriptions: %v", err) - } - nativeID, err := findNativeID(nativeSubscriptions, account.NativeID) - if err != nil { - return fmt.Errorf("failed to find native subscription: %w", err) - } - - jobID, err := azure.Wrap(a.client).StartDisableNativeSubscriptionProtectionJob(ctx, nativeID, azure.VM, deleteSnapshots) - if err != nil { - return fmt.Errorf("failed to disable native subscription: %v", err) - } - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) - if err != nil { - return fmt.Errorf("failed to wait for task chain: %v", err) - } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) - } - case account.Features[0].Equal(core.FeatureExocompute) && account.Features[0].Status != core.StatusDisabled: - jobID, err := azure.Wrap(a.client).StartDisableCloudAccountJob(ctx, account.ID, account.Features[0].Feature) - if err != nil { - return fmt.Errorf("failed to disable subscription feature %q: %v", feature, err) - } - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) - if err != nil { - return fmt.Errorf("failed to wait for task chain: %v", err) - } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) - } + if err := a.disableFeature(ctx, account, feature, deleteSnapshots); err != nil { + return fmt.Errorf("failed to disable subscripition feature %s: %s", feature, err) } err = azure.Wrap(a.client).DeleteCloudAccountWithoutOAuth(ctx, account.ID, feature) if err != nil { - return fmt.Errorf("failed to delete subscription feature %q: %v", feature, err) + return fmt.Errorf("failed to delete subscription feature %s: %s", feature, err) } return nil } -// findNativeID returns the RSC native subscription id for the given cloud -// account native subscription id. The cloud account subscription id is the -// same as the Azure subscription id. -func findNativeID(nativeSubscriptions []azure.NativeSubscription, nativeID uuid.UUID) (uuid.UUID, error) { - for _, subscription := range nativeSubscriptions { - if subscription.NativeID == nativeID { - return subscription.ID, nil +// disableFeature disables the specified subscription feature. +func (a API) disableFeature(ctx context.Context, account CloudAccount, feature core.Feature, deleteSnapshots bool) error { + a.log.Print(log.Trace) + + // If the feature has not been onboarded or the feature is in the disabled + // or connecting state, there is no need to disable the feature. + if feature, ok := account.Feature(feature); ok { + if feature.Status == core.StatusDisabled || feature.Status == core.StatusConnecting { + return nil } + } else { + return nil } - return uuid.Nil, fmt.Errorf("subscription %w", graphql.ErrNotFound) + // The Cloud Native Archival and Cloud Native Archival Encryption features + // should not be disabled. + if feature.Equal(core.FeatureCloudNativeArchival) || feature.Equal(core.FeatureCloudNativeArchivalEncryption) { + return nil + } + + jobID, err := azure.Wrap(a.client).StartDisableCloudAccountJob(ctx, account.ID, feature) + if err != nil { + return fmt.Errorf("failed to disable feature %s: %s", feature, err) + } + + if err := core.Wrap(a.client).WaitForFeatureDisableTaskChain(ctx, jobID, func(ctx context.Context) (bool, error) { + account, err := a.Subscription(ctx, CloudAccountID(account.ID), feature) + if err != nil { + return false, fmt.Errorf("failed to retrieve status for feature %s: %s", feature, err) + } + + feature, ok := account.Feature(feature) + if !ok { + return false, fmt.Errorf("failed to retrieve status for feature %s: not found", feature) + } + return feature.Status == core.StatusDisabled, nil + }); err != nil { + return fmt.Errorf("failed to wait for task chain %s: %s", jobID, err) + } + + return nil } -// UpdateSubscription updates the subscription with the specified id and feature. +// UpdateSubscription updates the subscription with the specified ID and feature. func (a API) UpdateSubscription(ctx context.Context, id IdentityFunc, feature core.Feature, opts ...OptionFunc) error { a.log.Print(log.Trace) @@ -520,10 +543,7 @@ func (a API) UpdateSubscription(ctx context.Context, id IdentityFunc, feature co var remove []azure.Region for _, region := range accountFeature.Regions { - reg, err := azure.ParseRegion(region) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } + reg := azure.RegionFromName(region) if _, ok := regions[reg]; ok { delete(regions, reg) } else { @@ -548,7 +568,7 @@ func (a API) UpdateSubscription(ctx context.Context, id IdentityFunc, feature co // AddServicePrincipal adds the service principal for the app. If shouldReplace // is true and the app already has a service principal, it will be replaced. // Note that it's not possible to remove a service principal once it has been -// set. Returns the application id of the service principal set. +// set. Returns the application ID of the service principal set. func (a API) AddServicePrincipal(ctx context.Context, principal ServicePrincipalFunc, shouldReplace bool) (uuid.UUID, error) { a.log.Print(log.Trace) @@ -569,9 +589,118 @@ func (a API) AddServicePrincipal(ctx context.Context, principal ServicePrincipal // SetServicePrincipal sets the service principal for the app. If the app // already has a service principal, it will be replaced. Note that it's not // possible to remove a service principal once it has been set. Returns the -// application id of the service principal set. +// application ID of the service principal set. func (a API) SetServicePrincipal(ctx context.Context, principal ServicePrincipalFunc) (uuid.UUID, error) { a.log.Print(log.Trace) return a.AddServicePrincipal(ctx, principal, true) } + +// toSubscriptions returns the unique subscriptions found in the rawTenants +// slice. This function requires that the tenants include subscription details. +func toSubscriptions(rawTenants []azure.CloudAccountTenant) []CloudAccount { + type tenantAccounts struct { + tenant CloudAccountTenant + accounts map[uuid.UUID]*CloudAccount + } + + tenantSet := make(map[uuid.UUID]*tenantAccounts) + for _, rawTenant := range rawTenants { + tenant, ok := tenantSet[rawTenant.ID] + if !ok { + tenantSet[rawTenant.ID] = &tenantAccounts{ + tenant: CloudAccountTenant{ + Cloud: string(rawTenant.Cloud), + ID: rawTenant.ID, + ClientID: rawTenant.ClientID, + AppName: rawTenant.AppName, + DomainName: rawTenant.DomainName, + SubscriptionCount: rawTenant.SubscriptionCount, + }, + accounts: make(map[uuid.UUID]*CloudAccount), + } + tenant = tenantSet[rawTenant.ID] + } + + for _, rawAccount := range rawTenant.Accounts { + account, ok := tenant.accounts[rawAccount.ID] + if !ok { + tenant.accounts[rawAccount.ID] = &CloudAccount{ + ID: rawAccount.ID, + NativeID: rawAccount.NativeID, + Name: rawAccount.Name, + TenantID: rawTenant.ID, + TenantDomain: rawTenant.DomainName, + } + account = tenant.accounts[rawAccount.ID] + } + + feature := core.Feature{Name: rawAccount.Feature.Feature} + if _, ok := account.Feature(feature); !ok { + tags := make(map[string]string, len(rawAccount.Feature.ResourceGroup.Tags)) + for _, tag := range rawAccount.Feature.ResourceGroup.Tags { + tags[tag.Key] = tag.Value + } + regions := make([]string, 0, len(rawAccount.Feature.Regions)) + for _, region := range rawAccount.Feature.Regions { + regions = append(regions, region.Name()) + } + account.Features = append(account.Features, Feature{ + Feature: feature, + ResourceGroup: FeatureResourceGroup{ + Name: rawAccount.Feature.ResourceGroup.Name, + NativeID: rawAccount.Feature.ResourceGroup.NativeID, + Tags: tags, + Region: rawAccount.Feature.ResourceGroup.Region.Name(), + }, + Regions: regions, + Status: rawAccount.Feature.Status, + UserAssignedManagedIdentity: FeatureUserAssignedManagedIdentity{ + Name: rawAccount.Feature.UserAssignedManagedIdentity.Name, + NativeID: rawAccount.Feature.UserAssignedManagedIdentity.NativeId, + PrincipalID: rawAccount.Feature.UserAssignedManagedIdentity.PrincipalID, + }, + }) + } + } + } + + var accounts []CloudAccount + for _, tenant := range tenantSet { + for _, account := range tenant.accounts { + accounts = append(accounts, *account) + } + } + slices.SortFunc(accounts, func(i, j CloudAccount) int { + return cmp.Compare(i.Name, j.Name) + }) + + return accounts +} + +// toTenants returns the unique tenants found in the rawTenants slice. +func toTenants(rawTenants []azure.CloudAccountTenant) []CloudAccountTenant { + tenantSet := make(map[uuid.UUID]CloudAccountTenant) + for _, rawTenant := range rawTenants { + if _, ok := tenantSet[rawTenant.ID]; !ok { + tenantSet[rawTenant.ID] = CloudAccountTenant{ + Cloud: string(rawTenant.Cloud), + ID: rawTenant.ID, + ClientID: rawTenant.ClientID, + AppName: rawTenant.AppName, + DomainName: rawTenant.DomainName, + SubscriptionCount: rawTenant.SubscriptionCount, + } + } + } + + tenants := make([]CloudAccountTenant, 0, len(tenantSet)) + for _, tenant := range tenantSet { + tenants = append(tenants, tenant) + } + slices.SortFunc(tenants, func(i, j CloudAccountTenant) int { + return cmp.Compare(i.DomainName, j.DomainName) + }) + + return tenants +} diff --git a/pkg/polaris/azure/azure_test.go b/pkg/polaris/azure/azure_test.go index 815334d7..f987984e 100644 --- a/pkg/polaris/azure/azure_test.go +++ b/pkg/polaris/azure/azure_test.go @@ -22,26 +22,31 @@ package azure import ( "context" + "encoding/json" "errors" "fmt" + "maps" "os" "reflect" + "slices" "testing" + "github.com/google/uuid" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/internal/testsetup" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -// client is the common RSC client used for tests. By reusing the same client -// we reduce the risk of hitting rate limits when access tokens are created. +// client is the common RSC client used for tests. Reusing the same client +// reduces the risk of hitting rate limits when access tokens are created. var client *polaris.Client func TestMain(m *testing.M) { if testsetup.BoolEnvSet("TEST_INTEGRATION") { - // Load configuration and create client. Usually resolved using the + // Load configuration and create the client. Usually resolved using the // environment variable RUBRIK_POLARIS_SERVICEACCOUNT_FILE. polAccount, err := polaris.DefaultServiceAccount(true) if err != nil { @@ -78,8 +83,8 @@ func TestMain(m *testing.M) { // TestAzureSubscriptionAddAndRemove verifies that the SDK can perform the // basic Azure subscription operations on a real RSC instance. // -// To run this test against an RSC instance the following environment variables -// needs to be set: +// To run this test against an RSC instance, the following environment variables +// need to be set: // - RUBRIK_POLARIS_SERVICEACCOUNT_FILE= // - TEST_INTEGRATION=1 // - TEST_AZURESUBSCRIPTION_FILE= @@ -108,10 +113,13 @@ func TestAzureSubscriptionAddAndRemove(t *testing.T) { t.Fatal(err) } - // Add default Azure subscription to RSC. + // Add Azure subscription to RSC. subscription := Subscription(testSubscription.SubscriptionID, testSubscription.TenantDomain) + cnpRegions := Regions(testSubscription.CloudNativeProtection.Regions...) + cnpResourceGroup := ResourceGroup(testSubscription.CloudNativeProtection.ResourceGroupName, + testSubscription.CloudNativeProtection.ResourceGroupRegion, nil) id, err := azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeProtection, - Regions("eastus2"), Name(testSubscription.SubscriptionName)) + Name(testSubscription.SubscriptionName), cnpRegions, cnpResourceGroup) if err != nil { t.Fatal(err) } @@ -121,29 +129,44 @@ func TestAzureSubscriptionAddAndRemove(t *testing.T) { if err != nil { t.Fatal(err) } - if account.Name != testSubscription.SubscriptionName { - t.Fatalf("invalid name: %v", account.Name) + if name := account.Name; name != testSubscription.SubscriptionName { + t.Fatalf("invalid name: %v", name) } - if account.NativeID != testSubscription.SubscriptionID { - t.Fatalf("invalid native id: %v", account.NativeID) + if id := account.NativeID; id != testSubscription.SubscriptionID { + t.Fatalf("invalid native id: %v", id) } - if account.TenantDomain != testSubscription.TenantDomain { - t.Fatalf("invalid tenant domain: %v", account.TenantDomain) + if domain := account.TenantDomain; domain != testSubscription.TenantDomain { + t.Fatalf("invalid tenant domain: %v", domain) } if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } - if !account.Features[0].Equal(core.FeatureCloudNativeProtection) { - t.Fatalf("invalid feature name: %v", account.Features[0].Name) + feature, ok := account.Feature(core.FeatureCloudNativeProtection) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeProtection) } - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"eastus2"}) { + if name := feature.Name; name != core.FeatureCloudNativeProtection.Name { + t.Fatalf("invalid feature name: %v", name) + } + slices.Sort(feature.Regions) + slices.Sort(testSubscription.CloudNativeProtection.Regions) + if regions := feature.Regions; !slices.Equal(regions, testSubscription.CloudNativeProtection.Regions) { t.Fatalf("invalid feature regions: %v", regions) } - if account.Features[0].Status != "CONNECTED" { - t.Fatalf("invalid feature status: %v", account.Features[0].Status) + if status := feature.Status; status != "CONNECTED" { + t.Fatalf("invalid feature status: %v", status) + } + if name := feature.ResourceGroup.Name; name != testSubscription.CloudNativeProtection.ResourceGroupName { + t.Fatalf("invalid feature resource group name: %v", name) + } + if region := feature.ResourceGroup.Region; region != testSubscription.CloudNativeProtection.ResourceGroupRegion { + t.Fatalf("invalid feature resource group region: %v", region) + } + if tags := feature.ResourceGroup.Tags; !maps.Equal(tags, map[string]string{}) { + t.Fatalf("invalid feature resource group tags: %v", tags) } - // Update and verify regions for Azure account. + // Update and verify regions for the Azure subscription. err = azureClient.UpdateSubscription(ctx, ID(subscription), core.FeatureCloudNativeProtection, Regions("westus2")) if err != nil { @@ -156,8 +179,13 @@ func TestAzureSubscriptionAddAndRemove(t *testing.T) { if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"westus2"}) { - t.Errorf("invalid feature regions: %v", regions) + feature, ok = account.Feature(core.FeatureCloudNativeProtection) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeProtection) + } + slices.Sort(feature.Regions) + if regions := feature.Regions; !slices.Equal(regions, []string{"westus2"}) { + t.Fatalf("invalid feature regions: %v", regions) } // Remove the Azure subscription from RSC keeping the snapshots. @@ -177,8 +205,8 @@ func TestAzureSubscriptionAddAndRemove(t *testing.T) { // the adding and removal of Azure subscription for archival feature on a real // RSC instance. // -// To run this test against an RSC instance the following environment variables -// needs to be set: +// To run this test against an RSC instance, the following environment variables +// need to be set: // - RUBRIK_POLARIS_SERVICEACCOUNT_FILE= // - TEST_INTEGRATION=1 // - TEST_AZURESUBSCRIPTION_FILE= @@ -207,68 +235,80 @@ func TestAzureArchivalSubscriptionAddAndRemove(t *testing.T) { t.Fatal(err) } - // Add default Azure subscription to RSC. + // Add Azure subscription to RSC. subscription := Subscription(testSubscription.SubscriptionID, testSubscription.TenantDomain) - id, err := azureClient.AddSubscription( - ctx, - subscription, - core.FeatureCloudNativeArchival, - Regions("eastus2"), - Name(testSubscription.SubscriptionName), - ResourceGroup(testSubscription.Archival.ResourceGroupName, "eastus2", make(map[string]string)), - ) + arcRegions := Regions(testSubscription.Archival.Regions...) + arcResourceGroup := ResourceGroup(testSubscription.Archival.ResourceGroupName, + testSubscription.Archival.ResourceGroupRegion, nil) + id, err := azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeArchival, + Name(testSubscription.SubscriptionName), arcRegions, arcResourceGroup) if err != nil { t.Fatal(err) } // Verify that the subscription was successfully added. - account, err := azureClient.Subscription( - ctx, - CloudAccountID(id), - core.FeatureCloudNativeArchival, - ) + account, err := azureClient.Subscription(ctx, CloudAccountID(id), core.FeatureCloudNativeArchival) if err != nil { t.Fatal(err) } - if account.Name != testSubscription.SubscriptionName { - t.Fatalf("invalid name: %v", account.Name) + if name := account.Name; name != testSubscription.SubscriptionName { + t.Fatalf("invalid name: %v", name) } - if account.NativeID != testSubscription.SubscriptionID { - t.Fatalf("invalid native id: %v", account.NativeID) + if id := account.NativeID; id != testSubscription.SubscriptionID { + t.Fatalf("invalid native id: %v", id) } - if account.TenantDomain != testSubscription.TenantDomain { - t.Fatalf("invalid tenant domain: %v", account.TenantDomain) + if domain := account.TenantDomain; domain != testSubscription.TenantDomain { + t.Fatalf("invalid tenant domain: %v", domain) } if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } - if !account.Features[0].Equal(core.FeatureCloudNativeArchival) { - t.Fatalf("invalid feature name: %v", account.Features[0].Name) + feature, ok := account.Feature(core.FeatureCloudNativeArchival) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeArchival) } - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"eastus2"}) { + if name := feature.Name; name != core.FeatureCloudNativeArchival.Name { + t.Fatalf("invalid feature name: %v", name) + } + slices.Sort(feature.Regions) + slices.Sort(testSubscription.Archival.Regions) + if regions := feature.Regions; !slices.Equal(regions, testSubscription.Archival.Regions) { t.Fatalf("invalid feature regions: %v", regions) } - if account.Features[0].Status != core.StatusConnected { - t.Fatalf("invalid feature status: %v", account.Features[0].Status) + if status := feature.Status; status != "CONNECTED" { + t.Fatalf("invalid feature status: %v", status) + } + if name := feature.ResourceGroup.Name; name != testSubscription.Archival.ResourceGroupName { + t.Fatalf("invalid feature resource group name: %v", name) + } + if region := feature.ResourceGroup.Region; region != testSubscription.Archival.ResourceGroupRegion { + t.Fatalf("invalid feature resource group region: %v", region) + } + if tags := feature.ResourceGroup.Tags; !maps.Equal(tags, map[string]string{}) { + t.Fatalf("invalid feature resource group tags: %v", tags) } - // Update and verify regions for Azure account. + // Update and verify regions for Azure subscription. err = azureClient.UpdateSubscription(ctx, ID(subscription), core.FeatureCloudNativeArchival, Regions("westus2")) if err != nil { - t.Error(err) + t.Fatal(err) } account, err = azureClient.Subscription(ctx, ID(subscription), core.FeatureCloudNativeArchival) if err != nil { - t.Error(err) + t.Fatal(err) } - if n := len(account.Features); n == 1 { - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"westus2"}) { - t.Fatalf("invalid feature regions: %v", regions) - } - } else { + if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } + feature, ok = account.Feature(core.FeatureCloudNativeArchival) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeArchival) + } + slices.Sort(feature.Regions) + if regions := feature.Regions; !slices.Equal(regions, []string{"westus2"}) { + t.Fatalf("invalid feature regions: %v", regions) + } // Remove the Azure subscription from RSC keeping the snapshots. err = azureClient.RemoveSubscription(ctx, ID(subscription), core.FeatureCloudNativeArchival, false) @@ -287,8 +327,8 @@ func TestAzureArchivalSubscriptionAddAndRemove(t *testing.T) { // can perform the adding and removal of Azure subscription for archival // encryption feature on a real RSC instance. // -// To run this test against an RSC instance the following environment variables -// needs to be set: +// To run this test against an RSC instance, the following environment variables +// need to be set: // - RUBRIK_POLARIS_SERVICEACCOUNT_FILE= // - TEST_INTEGRATION=1 // - TEST_AZURESUBSCRIPTION_FILE= @@ -320,74 +360,69 @@ func TestAzureArchivalEncryptionSubscriptionAddAndRemove(t *testing.T) { // Add subscription with archival feature as archival encryption is a child // feature and cannot be added without that. subscription := Subscription(testSubscription.SubscriptionID, testSubscription.TenantDomain) - _, err = azureClient.AddSubscription( - ctx, - subscription, - core.FeatureCloudNativeArchival, - Regions("eastus2"), - Name(testSubscription.SubscriptionName), - ResourceGroup(testSubscription.Archival.ResourceGroupName, "eastus2", make(map[string]string)), - ) + arcRegions := Regions(testSubscription.Archival.Regions...) + arcResourceGroup := ResourceGroup(testSubscription.Archival.ResourceGroupName, + testSubscription.Archival.ResourceGroupRegion, nil) + _, err = azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeArchival, + Name(testSubscription.SubscriptionName), arcRegions, arcResourceGroup) if err != nil { t.Fatal(err) } // Add archival encryption feature. - subscription = Subscription(testSubscription.SubscriptionID, testSubscription.TenantDomain) - id, err := azureClient.AddSubscription( - ctx, - subscription, - core.FeatureCloudNativeArchivalEncryption, - Regions("eastus2"), - Name(testSubscription.SubscriptionName), - ManagedIdentity( - testSubscription.Archival.ManagedIdentityName, - testSubscription.Archival.ResourceGroupName, - testSubscription.Archival.PrincipalID, - "eastus2", - ), - ResourceGroup( - testSubscription.Archival.ResourceGroupName, - "eastus2", - make(map[string]string), - ), - ) + encManagedIdentity := ManagedIdentity(testSubscription.Archival.ManagedIdentityName, + testSubscription.Archival.ResourceGroupName, testSubscription.Archival.PrincipalID, + testSubscription.Archival.ResourceGroupRegion) + id, err := azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeArchivalEncryption, + Name(testSubscription.SubscriptionName), arcRegions, arcResourceGroup, encManagedIdentity) if err != nil { t.Fatal(err) } // Verify that the subscription was successfully added. - account, err := azureClient.Subscription( - ctx, - CloudAccountID(id), - core.FeatureCloudNativeArchivalEncryption, - ) + account, err := azureClient.Subscription(ctx, CloudAccountID(id), core.FeatureCloudNativeArchivalEncryption) if err != nil { t.Fatal(err) } - if account.Name != testSubscription.SubscriptionName { - t.Fatalf("invalid name: %v", account.Name) + + if name := account.Name; name != testSubscription.SubscriptionName { + t.Fatalf("invalid name: %v", name) } - if account.NativeID != testSubscription.SubscriptionID { - t.Fatalf("invalid native id: %v", account.NativeID) + if id := account.NativeID; id != testSubscription.SubscriptionID { + t.Fatalf("invalid native id: %v", id) } - if account.TenantDomain != testSubscription.TenantDomain { - t.Fatalf("invalid tenant domain: %v", account.TenantDomain) + if domain := account.TenantDomain; domain != testSubscription.TenantDomain { + t.Fatalf("invalid tenant domain: %v", domain) } if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } - if !account.Features[0].Equal(core.FeatureCloudNativeArchivalEncryption) { - t.Fatalf("invalid feature name: %v", account.Features[0].Name) + feature, ok := account.Feature(core.FeatureCloudNativeArchivalEncryption) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeArchivalEncryption) } - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"eastus2"}) { + if name := feature.Name; name != core.FeatureCloudNativeArchivalEncryption.Name { + t.Fatalf("invalid feature name: %v", name) + } + slices.Sort(feature.Regions) + slices.Sort(testSubscription.Archival.Regions) + if regions := feature.Regions; !slices.Equal(regions, testSubscription.Archival.Regions) { t.Fatalf("invalid feature regions: %v", regions) } - if account.Features[0].Status != core.StatusConnected { - t.Fatalf("invalid feature status: %v", account.Features[0].Status) + if status := feature.Status; status != "CONNECTED" { + t.Fatalf("invalid feature status: %v", status) + } + if name := feature.ResourceGroup.Name; name != testSubscription.Archival.ResourceGroupName { + t.Fatalf("invalid feature resource group name: %v", name) + } + if region := feature.ResourceGroup.Region; region != testSubscription.Archival.ResourceGroupRegion { + t.Fatalf("invalid feature resource group region: %v", region) + } + if tags := feature.ResourceGroup.Tags; !maps.Equal(tags, map[string]string{}) { + t.Fatalf("invalid feature resource group tags: %v", tags) } - // Update and verify regions for Azure account. + // Update and verify regions for the Azure account. err = azureClient.UpdateSubscription(ctx, ID(subscription), core.FeatureCloudNativeArchivalEncryption, Regions("westus2")) if err != nil { @@ -397,13 +432,17 @@ func TestAzureArchivalEncryptionSubscriptionAddAndRemove(t *testing.T) { if err != nil { t.Fatal(err) } - if n := len(account.Features); n == 1 { - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"westus2"}) { - t.Fatalf("invalid feature regions: %v", regions) - } - } else { + if n := len(account.Features); n != 1 { t.Fatalf("invalid number of features: %v", n) } + feature, ok = account.Feature(core.FeatureCloudNativeArchivalEncryption) + if !ok { + t.Fatalf("%s feature not found", core.FeatureCloudNativeArchivalEncryption) + } + slices.Sort(feature.Regions) + if regions := feature.Regions; !slices.Equal(regions, []string{"westus2"}) { + t.Fatalf("invalid feature regions: %v", regions) + } // Remove the Azure subscription from RSC keeping the snapshots. Removing // archival feature as encryption is a child feature of it. @@ -418,3 +457,207 @@ func TestAzureArchivalEncryptionSubscriptionAddAndRemove(t *testing.T) { t.Fatal(err) } } + +func TestToSubscription(t *testing.T) { + rawTenants, err := allAzureCloudAccountTenantsResponse() + if err != nil { + t.Fatal(err) + } + + subs := toSubscriptions(rawTenants) + if n := len(subs); n != 3 { + t.Fatalf("invalid number of subscriptions: %v", n) + } + + // Subscription 1. + if subs[0].ID != uuid.MustParse("f4b69681-2ab8-4edc-81c2-8852e46c1ba3") { + t.Errorf("invalid id: %v", subs[0].ID) + } + if subs[0].NativeID != uuid.MustParse("c263212c-3f26-4b9a-8601-9efb466c8837") { + t.Errorf("invalid native id: %v", subs[0].NativeID) + } + if subs[0].Name != "subscription1" { + t.Errorf("invalid name: %v", subs[0].Name) + } + if subs[0].TenantID != uuid.MustParse("ca997d29-1811-4aab-a5dc-649082debe89") { + t.Errorf("invalid tenant id: %v", subs[0].TenantID) + } + if subs[0].TenantDomain != "domain1.onmicrosoft.com" { + t.Errorf("invalid tenant domain: %v", subs[0].TenantDomain) + } + if !reflect.DeepEqual(subs[0].Features, []Feature{{ + Feature: core.Feature{Name: "CLOUD_NATIVE_PROTECTION"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg1", + NativeID: "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg1", + Region: "westus", + Tags: map[string]string{}, + }, + Regions: []string{"eastus", "westus"}, + Status: "MISSING_PERMISSIONS", + }, { + Feature: core.Feature{Name: "EXOCOMPUTE"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg2", + NativeID: "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg2", + Region: "westus", + Tags: map[string]string{}, + }, + Regions: []string{"westus"}, + Status: "CONNECTED", + }}) { + t.Errorf("invalid features: %v", subs[0].Features) + } + + // Subscription 2. + if subs[1].ID != uuid.MustParse("e2e3fb63-2230-4154-9b1b-923f018dbc4f") { + t.Errorf("invalid id: %v", subs[1].ID) + } + if subs[1].NativeID != uuid.MustParse("1ee74f16-10d3-45fe-adfb-7f70ee77f5ee") { + t.Errorf("invalid native id: %v", subs[1].NativeID) + } + if subs[1].Name != "subscription2" { + t.Errorf("invalid name: %v", subs[1].Name) + } + if subs[1].TenantID != uuid.MustParse("88af4472-ea52-4c8e-bf05-e4ca581370a7") { + t.Errorf("invalid tenant id: %v", subs[1].TenantID) + } + if subs[1].TenantDomain != "domain2.onmicrosoft.com" { + t.Errorf("invalid tenant domain: %v", subs[1].TenantDomain) + } + if !reflect.DeepEqual(subs[1].Features, []Feature{{ + Feature: core.Feature{Name: "CLOUD_NATIVE_PROTECTION"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg3", + NativeID: "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg3", + Region: "westus2", + Tags: map[string]string{}, + }, + Regions: []string{"westus2"}, + Status: "MISSING_PERMISSIONS", + }, { + Feature: core.Feature{Name: "EXOCOMPUTE"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg4", + NativeID: "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg4", + Region: "westus2", + Tags: map[string]string{}, + }, + Regions: []string{}, + Status: "CONNECTED", + }}) { + t.Errorf("invalid features: %v", subs[1].Features) + } + + // Subscription 3. + if subs[2].ID != uuid.MustParse("31116cf6-6259-4cfc-b8a6-307cb0744ba1") { + t.Errorf("invalid id: %v", subs[2].ID) + } + if subs[2].NativeID != uuid.MustParse("973bfa00-0bfd-4850-aab1-ebd3f9d9b6b7") { + t.Errorf("invalid native id: %v", subs[2].NativeID) + } + if subs[2].Name != "subscription3" { + t.Errorf("invalid name: %v", subs[2].Name) + } + if subs[2].TenantID != uuid.MustParse("88af4472-ea52-4c8e-bf05-e4ca581370a7") { + t.Errorf("invalid tenant id: %v", subs[2].TenantID) + } + if subs[2].TenantDomain != "domain2.onmicrosoft.com" { + t.Errorf("invalid tenant domain: %v", subs[2].TenantDomain) + } + if !reflect.DeepEqual(subs[2].Features, []Feature{{ + Feature: core.Feature{Name: "CLOUD_NATIVE_PROTECTION"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg5", + NativeID: "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + Region: "westus", + Tags: map[string]string{ + "key1": "value1", + }, + }, + Regions: []string{"westus"}, + Status: "MISSING_PERMISSIONS", + }, { + Feature: core.Feature{Name: "EXOCOMPUTE"}, + ResourceGroup: FeatureResourceGroup{ + Name: "rg5", + NativeID: "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + Region: "westus", + Tags: map[string]string{}, + }, + Regions: []string{"westus"}, + Status: "MISSING_PERMISSIONS", + }}) { + t.Errorf("invalid features: %v", subs[2].Features) + } +} + +func TestToTenant(t *testing.T) { + rawTenants, err := allAzureCloudAccountTenantsResponse() + if err != nil { + t.Fatal(err) + } + + tenants := toTenants(rawTenants) + if n := len(tenants); n != 2 { + t.Errorf("invalid number of tenants: %v", n) + } + + // Tenant 1. + if tenants[0].Cloud != "AZUREPUBLICCLOUD" { + t.Errorf("invalid cloud: %v", tenants[0].Cloud) + } + if tenants[0].ID != uuid.MustParse("ca997d29-1811-4aab-a5dc-649082debe89") { + t.Errorf("invalid id: %v", tenants[0].ID) + } + if tenants[0].ClientID != uuid.MustParse("b6a26799-b722-4df6-b2df-9c70433ee55f") { + t.Errorf("invalid client id: %v", tenants[0].ClientID) + } + if tenants[0].AppName != "app1" { + t.Errorf("invalid app name: %v", tenants[0].AppName) + } + if tenants[0].DomainName != "domain1.onmicrosoft.com" { + t.Errorf("invalid domain: %v", tenants[0].DomainName) + } + if tenants[0].SubscriptionCount != 1 { + t.Errorf("invalid subscription count: %v", tenants[0].SubscriptionCount) + } + + // Tenant 2. + if tenants[1].Cloud != "AZUREPUBLICCLOUD" { + t.Errorf("invalid cloud: %v", tenants[1].Cloud) + } + if tenants[1].ID != uuid.MustParse("88af4472-ea52-4c8e-bf05-e4ca581370a7") { + t.Errorf("invalid id: %v", tenants[1].ID) + } + if tenants[1].ClientID != uuid.MustParse("6688f45e-b1dc-41d8-b926-3acef4a4beaf") { + t.Errorf("invalid client id: %v", tenants[1].ClientID) + } + if tenants[1].AppName != "app2" { + t.Errorf("invalid app name: %v", tenants[1].AppName) + } + if tenants[1].DomainName != "domain2.onmicrosoft.com" { + t.Errorf("invalid domain: %v", tenants[1].DomainName) + } + if tenants[1].SubscriptionCount != 2 { + t.Errorf("invalid subscription count: %v", tenants[1].SubscriptionCount) + } +} + +func allAzureCloudAccountTenantsResponse() ([]azure.CloudAccountTenant, error) { + buf, err := os.ReadFile("testdata/all_azure_cloud_account_tenants_response.json") + if err != nil { + return nil, err + } + + var payload struct { + Data struct { + Result []azure.CloudAccountTenant `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, err + } + + return payload.Data.Result, nil +} diff --git a/pkg/polaris/azure/exocompute.go b/pkg/polaris/azure/exocompute.go index 04e75d77..8be2815b 100644 --- a/pkg/polaris/azure/exocompute.go +++ b/pkg/polaris/azure/exocompute.go @@ -25,67 +25,98 @@ import ( "fmt" "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/exocompute" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -// ExocomputeConfig represents a single exocompute config. +// ExocomputeConfig represents a single exocompute configuration. type ExocomputeConfig struct { - ID uuid.UUID - Region string - SubnetID string + ID uuid.UUID // Rubrik exocompute configuration ID. + Region string + SubnetID string // Azure subnet ID. + ManagedByRubrik bool // When true, Rubrik will manage the security groups. + PodOverlayNetworkCIDR string + PodSubnetID string // Azure subnet ID. + HealthCheckStatus HealthCheckStatus // Health status of the exocompute cluster. +} - // When true Rubrik will manage the security groups. - ManagedByRubrik bool +// HealthCheckStatus represents the health status of an exocompute cluster. +type HealthCheckStatus struct { + Status string + FailureReason string + LastUpdatedAt string + TaskchainID string } -// ExoConfigFunc returns an exocompute config initialized from the values -// passed to the function creating the ExoConfigFunc. -type ExoConfigFunc func(ctx context.Context) (azure.ExocomputeConfigCreate, error) +// ExoConfigFunc returns an ExoCreateParams object initialized from the values +// passed to the function when creating the ExoConfigFunc. +type ExoConfigFunc func(ctx context.Context) (azure.ExoCreateParams, error) -// Managed returns an ExoConfigFunc that initializes an exocompute config with -// security groups managed by Rubrik using the specified values. +// Managed returns an ExoConfigFunc that initializes an ExoCreateParams object +// with the specified values. func Managed(region, subnetID string) ExoConfigFunc { - return func(ctx context.Context) (azure.ExocomputeConfigCreate, error) { - r, err := azure.ParseRegion(region) - if err != nil { - return azure.ExocomputeConfigCreate{}, fmt.Errorf("failed to parse region: %v", err) - } - return azure.ExocomputeConfigCreate{ - Region: r, - SubnetID: subnetID, + return func(ctx context.Context) (azure.ExoCreateParams, error) { + return azure.ExoCreateParams{ IsManagedByRubrik: true, + Region: azure.RegionFromName(region).ToCloudAccountRegionEnum(), + SubnetID: subnetID, + }, nil + } +} + +// ManagedWithOverlayNetwork returns an ExoConfigFunc that initializes an +// ExoCreateParams object with the specified values. +func ManagedWithOverlayNetwork(region, subnetID, podOverlayNetworkCIDR string) ExoConfigFunc { + return func(ctx context.Context) (azure.ExoCreateParams, error) { + return azure.ExoCreateParams{ + IsManagedByRubrik: true, + Region: azure.RegionFromName(region).ToCloudAccountRegionEnum(), + SubnetID: subnetID, + PodOverlayNetworkCIDR: podOverlayNetworkCIDR, }, nil } } // toExocomputeConfig converts an polaris/graphql/azure exocompute config to an // polaris/azure exocompute config. -func toExocomputeConfig(config azure.ExocomputeConfig) ExocomputeConfig { +func toExocomputeConfig(configID uuid.UUID, config azure.ExoConfig) ExocomputeConfig { return ExocomputeConfig{ - ID: config.ID, - Region: azure.FormatRegion(config.Region), - SubnetID: config.SubnetID, - ManagedByRubrik: config.IsManagedByRubrik, + ID: configID, + Region: config.Region.Name(), + SubnetID: config.SubnetID, + ManagedByRubrik: config.ManagedByRubrik, + PodOverlayNetworkCIDR: config.PodOverlayNetworkCIDR, + PodSubnetID: config.PodSubnetID, + HealthCheckStatus: HealthCheckStatus{ + Status: config.HealthCheckStatus.Status, + FailureReason: config.HealthCheckStatus.FailureReason, + LastUpdatedAt: config.HealthCheckStatus.LastUpdatedAt, + TaskchainID: config.HealthCheckStatus.TaskchainID, + }, } } // ExocomputeConfig returns the exocompute config with the specified exocompute -// config id. -func (a API) ExocomputeConfig(ctx context.Context, id uuid.UUID) (ExocomputeConfig, error) { +// config ID. +func (a API) ExocomputeConfig(ctx context.Context, configID uuid.UUID) (ExocomputeConfig, error) { a.log.Print(log.Trace) - configsForAccounts, err := azure.Wrap(a.client).ExocomputeConfigs(ctx, "") + configsForAccounts, err := exocompute.ListConfigurations[azure.ExoConfigsForAccount](ctx, a.client, "") if err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to get exocompute configs: %v", err) + return ExocomputeConfig{}, fmt.Errorf("failed to get exocompute configs: %s", err) } - for _, configsForAccount := range configsForAccounts { for _, config := range configsForAccount.Configs { - if config.ID == id { - return toExocomputeConfig(config), nil + id, err := uuid.Parse(config.ID) + if err != nil { + return ExocomputeConfig{}, fmt.Errorf("failed to parse exocompute config id: %s", err) + } + if id == configID { + return toExocomputeConfig(id, config), nil } } } @@ -94,24 +125,27 @@ func (a API) ExocomputeConfig(ctx context.Context, id uuid.UUID) (ExocomputeConf } // ExocomputeConfigs returns all exocompute configs for the account with the -// specified id. +// specified ID. func (a API) ExocomputeConfigs(ctx context.Context, id IdentityFunc) ([]ExocomputeConfig, error) { a.log.Print(log.Trace) nativeID, err := a.toNativeID(ctx, id) if err != nil { - return nil, fmt.Errorf("failed to get native id: %v", err) + return nil, fmt.Errorf("failed to get native id: %s", err) } - configsForAccounts, err := azure.Wrap(a.client).ExocomputeConfigs(ctx, nativeID.String()) + configsForAccounts, err := exocompute.ListConfigurations[azure.ExoConfigsForAccount](ctx, a.client, nativeID.String()) if err != nil { - return nil, fmt.Errorf("failed to get exocompute configs: %v", err) + return nil, fmt.Errorf("failed to get exocompute configs: %s", err) } - var exoConfigs []ExocomputeConfig for _, configsForAccount := range configsForAccounts { for _, config := range configsForAccount.Configs { - exoConfigs = append(exoConfigs, toExocomputeConfig(config)) + id, err := uuid.Parse(config.ID) + if err != nil { + return nil, fmt.Errorf("failed to parse exocompute configuration id: %s", err) + } + exoConfigs = append(exoConfigs, toExocomputeConfig(id, config)) } } @@ -125,30 +159,92 @@ func (a API) AddExocomputeConfig(ctx context.Context, id IdentityFunc, config Ex exoConfig, err := config(ctx) if err != nil { - return uuid.Nil, fmt.Errorf("failed to lookup exocompute config: %v", err) + return uuid.Nil, fmt.Errorf("failed to lookup exocompute config: %s", err) } accountID, err := a.toCloudAccountID(ctx, id) if err != nil { - return uuid.Nil, fmt.Errorf("failed to get cloud account id: %v", err) + return uuid.Nil, fmt.Errorf("failed to get cloud account id: %s", err) } - exo, err := azure.Wrap(a.client).AddCloudAccountExocomputeConfigurations(ctx, accountID, exoConfig) + exoID, err := exocompute.CreateConfiguration[azure.ExoCreateResult](ctx, a.client, accountID, exoConfig) if err != nil { - return uuid.Nil, fmt.Errorf("failed to create exocompute config: %v", err) + return uuid.Nil, fmt.Errorf("failed to create exocompute config: %s", err) } - return exo.ID, nil + return exoID, nil } // RemoveExocomputeConfig removes the exocompute config with the specified // exocompute config id. -func (a API) RemoveExocomputeConfig(ctx context.Context, id uuid.UUID) error { +func (a API) RemoveExocomputeConfig(ctx context.Context, configID uuid.UUID) error { a.log.Print(log.Trace) - err := azure.Wrap(a.client).DeleteCloudAccountExocomputeConfigurations(ctx, id) + err := exocompute.DeleteConfiguration[azure.ExoDeleteResult](ctx, a.client, configID) if err != nil { - return fmt.Errorf("failed to remove exocompute config: %v", err) + return fmt.Errorf("failed to remove exocompute config: %s", err) + } + + return nil +} + +// ExocomputeHostAccount returns the exocompute host cloud account ID for the +// specified application cloud account. +func (a API) ExocomputeHostAccount(ctx context.Context, appID IdentityFunc) (uuid.UUID, error) { + a.log.Print(log.Trace) + + appCloudAccountID, err := a.toCloudAccountID(ctx, appID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get cloud account id: %s", err) + } + + mappings, err := exocompute.AllCloudAccountMappings(ctx, a.client, core.CloudVendorAzure) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get cloud account exocompute mappings: %s", err) + } + for _, mapping := range mappings { + if mapping.AppCloudAccountID == appCloudAccountID { + return mapping.HostCloudAccountID, nil + } + } + + return uuid.Nil, fmt.Errorf("exocompute account %w", graphql.ErrNotFound) +} + +// MapExocompute maps the exocompute application cloud account to the specified +// host cloud account. +func (a API) MapExocompute(ctx context.Context, hostID IdentityFunc, appID IdentityFunc) error { + a.log.Print(log.Trace) + + hostCloudAccountID, err := a.toCloudAccountID(ctx, hostID) + if err != nil { + return fmt.Errorf("failed to get cloud account id: %s", err) + } + + appCloudAccountID, err := a.toCloudAccountID(ctx, appID) + if err != nil { + return fmt.Errorf("failed to get cloud account id: %s", err) + } + + if err := exocompute.MapCloudAccount[azure.ExoMapResult](ctx, a.client, hostCloudAccountID, appCloudAccountID); err != nil { + return fmt.Errorf("failed to map exocompute config: %s", err) + } + + return nil +} + +// UnmapExocompute unmaps the exocompute application cloud account with the +// specified cloud account ID. +func (a API) UnmapExocompute(ctx context.Context, appID IdentityFunc) error { + a.log.Print(log.Trace) + + appCloudAccountID, err := a.toCloudAccountID(ctx, appID) + if err != nil { + return fmt.Errorf("failed to get cloud account id: %s", err) + } + + if err := exocompute.UnmapCloudAccount[azure.ExoUnmapResult](ctx, a.client, appCloudAccountID); err != nil { + return fmt.Errorf("failed to unmap exocompute config: %s", err) } return nil diff --git a/pkg/polaris/azure/exocompute_test.go b/pkg/polaris/azure/exocompute_test.go index aaabf508..cbe5c9d1 100644 --- a/pkg/polaris/azure/exocompute_test.go +++ b/pkg/polaris/azure/exocompute_test.go @@ -23,7 +23,8 @@ package azure import ( "context" "errors" - "reflect" + "maps" + "slices" "testing" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/internal/testsetup" @@ -34,8 +35,8 @@ import ( // TestAzureExocompute verifies that the SDK can perform basic Exocompute // operations on a real RSC instance. // -// To run this test against an RSC instance the following environment variables -// needs to be set: +// To run this test against an RSC instance, the following environment variables +// need to be set: // - RUBRIK_POLARIS_SERVICEACCOUNT_FILE= // - TEST_INTEGRATION=1 // - TEST_AZURESUBSCRIPTION_FILE= @@ -64,16 +65,23 @@ func TestAzureExocompute(t *testing.T) { t.Fatal(err) } - // Add default Azure subscription to RSC. + // Add Azure subscription to RSC. subscription := Subscription(testSubscription.SubscriptionID, testSubscription.TenantDomain) + cnpRegions := Regions(testSubscription.CloudNativeProtection.Regions...) + cnpResourceGroup := ResourceGroup(testSubscription.CloudNativeProtection.ResourceGroupName, + testSubscription.CloudNativeProtection.ResourceGroupRegion, nil) accountID, err := azureClient.AddSubscription(ctx, subscription, core.FeatureCloudNativeProtection, - Regions("eastus2"), Name(testSubscription.SubscriptionName)) + Name(testSubscription.SubscriptionName), cnpRegions, cnpResourceGroup) if err != nil { t.Fatal(err) } // Enable the exocompute feature for the account. - exoAccountID, err := azureClient.AddSubscription(ctx, subscription, core.FeatureExocompute, Regions("eastus2")) + exoRegions := Regions(testSubscription.Exocompute.Regions...) + exoResourceGroup := ResourceGroup(testSubscription.Exocompute.ResourceGroupName, + testSubscription.Exocompute.ResourceGroupRegion, nil) + exoAccountID, err := azureClient.AddSubscription(ctx, subscription, core.FeatureExocompute, exoRegions, + exoResourceGroup) if err != nil { t.Fatal(err) } @@ -83,78 +91,96 @@ func TestAzureExocompute(t *testing.T) { account, err := azureClient.Subscription(ctx, CloudAccountID(accountID), core.FeatureExocompute) if err != nil { - t.Error(err) + t.Fatal(err) + } + if name := account.Name; name != testSubscription.SubscriptionName { + t.Fatalf("invalid name: %v", name) + } + if id := account.NativeID; id != testSubscription.SubscriptionID { + t.Fatalf("invalid native id: %v", id) + } + if domain := account.TenantDomain; domain != testSubscription.TenantDomain { + t.Fatalf("invalid tenant domain: %v", domain) + } + if n := len(account.Features); n != 1 { + t.Fatalf("invalid number of features: %v", n) + } + feature, ok := account.Feature(core.FeatureExocompute) + if !ok { + t.Fatalf("%s feature not found", core.FeatureExocompute) + } + if name := feature.Name; name != core.FeatureExocompute.Name { + t.Fatalf("invalid feature name: %v", name) + } + slices.Sort(feature.Regions) + slices.Sort(testSubscription.Exocompute.Regions) + if regions := feature.Regions; !slices.Equal(regions, testSubscription.Exocompute.Regions) { + t.Fatalf("invalid feature regions: %v", regions) } - if account.Name != testSubscription.SubscriptionName { - t.Errorf("invalid name: %v", account.Name) + if status := feature.Status; status != "CONNECTED" { + t.Fatalf("invalid feature status: %v", status) } - if account.NativeID != testSubscription.SubscriptionID { - t.Errorf("invalid native id: %v", account.NativeID) + if name := feature.ResourceGroup.Name; name != testSubscription.Exocompute.ResourceGroupName { + t.Fatalf("invalid feature resource group name: %v", name) } - if account.TenantDomain != testSubscription.TenantDomain { - t.Errorf("invalid tenant domain: %v", account.TenantDomain) + if region := feature.ResourceGroup.Region; region != testSubscription.Exocompute.ResourceGroupRegion { + t.Fatalf("invalid feature resource group region: %v", region) } - if n := len(account.Features); n == 1 { - if feature := account.Features[0].Feature; !feature.Equal(core.FeatureExocompute) { - t.Errorf("invalid feature name: %v", feature) - } - if regions := account.Features[0].Regions; !reflect.DeepEqual(regions, []string{"eastus2"}) { - t.Errorf("invalid feature regions: %v", regions) - } - if account.Features[0].Status != core.StatusConnected { - t.Errorf("invalid feature status: %v", account.Features[0].Status) - } - } else { - t.Errorf("invalid number of features: %v", n) + if tags := feature.ResourceGroup.Tags; !maps.Equal(tags, map[string]string{}) { + t.Fatalf("invalid feature resource group tags: %v", tags) } // Add exocompute config for the account. + if len(testSubscription.Exocompute.Regions) == 0 { + t.Fatalf("exocompute test data must contain at least one region") + } + exoConfigRegion := testSubscription.Exocompute.Regions[0] exoID, err := azureClient.AddExocomputeConfig(ctx, CloudAccountID(accountID), - Managed("eastus2", testSubscription.Exocompute.SubnetID)) + Managed(exoConfigRegion, testSubscription.Exocompute.SubnetID)) if err != nil { - t.Error(err) + t.Fatal(err) } // Retrieve the exocompute config added. exoConfig, err := azureClient.ExocomputeConfig(ctx, exoID) if err != nil { - t.Error(err) + t.Fatal(err) } - if exoConfig.ID != exoID { - t.Errorf("invalid id: %v", exoConfig.ID) + if id := exoConfig.ID; id != exoID { + t.Fatalf("invalid exocompute config id: %v", id) } - if exoConfig.Region != "eastus2" { - t.Errorf("invalid region: %v", exoConfig.Region) + if region := exoConfig.Region; region != exoConfigRegion { + t.Fatalf("invalid exocompute config region: %v", region) } - if exoConfig.SubnetID != testSubscription.Exocompute.SubnetID { - t.Errorf("invalid subnet id: %v", exoConfig.SubnetID) + if id := exoConfig.SubnetID; id != testSubscription.Exocompute.SubnetID { + t.Fatalf("invalid exocompute config subnet id: %v", id) } if !exoConfig.ManagedByRubrik { - t.Errorf("invalid polaris managed state: %t", exoConfig.ManagedByRubrik) + t.Fatalf("invalid exocompute config managed state: %t", exoConfig.ManagedByRubrik) } // Remove the exocompute config. err = azureClient.RemoveExocomputeConfig(ctx, exoID) if err != nil { - t.Error(err) + t.Fatal(err) } // Verify that the exocompute config was successfully removed. exoConfig, err = azureClient.ExocomputeConfig(ctx, exoID) if !errors.Is(err, graphql.ErrNotFound) { - t.Error(err) + t.Fatal(err) } // Remove exocompute feature. err = azureClient.RemoveSubscription(ctx, CloudAccountID(accountID), core.FeatureExocompute, false) if err != nil { - t.Error(err) + t.Fatal(err) } // Verify that the feature was successfully removed. _, err = azureClient.Subscription(ctx, CloudAccountID(accountID), core.FeatureExocompute) if !errors.Is(err, graphql.ErrNotFound) { - t.Error(err) + t.Fatal(err) } // Remove subscription. diff --git a/pkg/polaris/azure/options.go b/pkg/polaris/azure/options.go index 05dd0458..89d8fc6a 100644 --- a/pkg/polaris/azure/options.go +++ b/pkg/polaris/azure/options.go @@ -25,7 +25,6 @@ import ( "fmt" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" - "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" ) type options struct { @@ -39,7 +38,7 @@ type options struct { // to the specified options instance. type OptionFunc func(ctx context.Context, opts *options) error -// Name returns an OptionFunc that gives the specified name to the options +// Name returns an OptionFunc that gives the specified name to the option // instance. func Name(name string) OptionFunc { return func(ctx context.Context, opts *options) error { @@ -48,22 +47,17 @@ func Name(name string) OptionFunc { } } -// Region returns an OptionFunc that gives the specified region to the options +// Region returns an OptionFunc that gives the specified region to the option // instance. func Region(region string) OptionFunc { return func(ctx context.Context, opts *options) error { - r, err := azure.ParseRegion(region) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } - - opts.regions = append(opts.regions, r) + opts.regions = append(opts.regions, azure.RegionFromName(region)) return nil } } -// Regions returns an OptionFunc that gives the specified regions to the -// options instance. +// Regions return an OptionFunc that gives the specified regions to the option +// instance. func Regions(regions ...string) OptionFunc { return func(ctx context.Context, opts *options) error { set := make(map[azure.Region]struct{}, len(regions)+len(opts.regions)) @@ -72,11 +66,7 @@ func Regions(regions ...string) OptionFunc { } for _, r := range regions { - region, err := azure.ParseRegion(r) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } - + region := azure.RegionFromName(r) if _, ok := set[region]; !ok { opts.regions = append(opts.regions, region) set[region] = struct{}{} @@ -88,17 +78,12 @@ func Regions(regions ...string) OptionFunc { } // ResourceGroup returns an OptionFunc that gives the specified resource group -// to the options instance. This is currently only needed for archival feature. -// Support will be added for other features soon. +// to the option instance. func ResourceGroup(name, region string, tags map[string]string) OptionFunc { return func(ctx context.Context, opts *options) error { if name == "" { return fmt.Errorf("invalid name for resource group") } - r, err := azure.ParseRegion(region) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } tagList := make([]azure.Tag, 0, len(tags)) for key, value := range tags { @@ -107,15 +92,15 @@ func ResourceGroup(name, region string, tags map[string]string) OptionFunc { opts.resourceGroup = &azure.ResourceGroup{ Name: name, - TagList: &azure.TagList{Tags: tagList}, - Region: r, + TagList: azure.TagList{Tags: tagList}, + Region: azure.RegionFromName(region).ToCloudAccountRegionEnum(), } return nil } } // ManagedIdentity returns an OptionFunc that gives the specified managed -// identity to the options instance. This is currently only needed for archival +// identity to the option instance. This is currently only needed for archival // encryption feature. func ManagedIdentity(name, resourceGroup, principalID, region string) OptionFunc { return func(ctx context.Context, opts *options) error { @@ -129,11 +114,6 @@ func ManagedIdentity(name, resourceGroup, principalID, region string) OptionFunc return fmt.Errorf("invalid principal ID for managed identity") } - r, err := azure.ParseRegion(region) - if err != nil { - return fmt.Errorf("failed to parse region: %v", err) - } - if opts.featureSpecificInfo == nil { opts.featureSpecificInfo = &azure.FeatureSpecificInfo{} } @@ -141,26 +121,8 @@ func ManagedIdentity(name, resourceGroup, principalID, region string) OptionFunc Name: name, ResourceGroupName: resourceGroup, PrincipalID: principalID, - Region: r, + Region: azure.RegionFromName(region).ToCloudAccountRegionEnum(), } return nil } } - -func verifyOptionsForFeature(opts options, feature core.Feature) error { - switch { - case feature.Equal(core.FeatureCloudNativeArchivalEncryption): - if opts.featureSpecificInfo == nil || - opts.featureSpecificInfo.UserAssignedManagedIdentity == nil { - return fmt.Errorf("managed identity should be added for archival encryption") - } - if opts.resourceGroup == nil { - return fmt.Errorf("resource group should be added for archival encryption") - } - case feature.Equal(core.FeatureCloudNativeArchival): - if opts.resourceGroup == nil { - return fmt.Errorf("resource group should be added for archival") - } - } - return nil -} diff --git a/pkg/polaris/azure/permissions.go b/pkg/polaris/azure/permissions.go index 8758be1a..3bf11a37 100644 --- a/pkg/polaris/azure/permissions.go +++ b/pkg/polaris/azure/permissions.go @@ -21,14 +21,30 @@ package azure import ( + "cmp" "context" "fmt" + "slices" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) +type Scope int + +const ( + // ScopeLegacy provides backwards compatibility with how permissions worked + // before scoped permissions were introduced. + ScopeLegacy Scope = iota + + // ScopeSubscription represents the subscription level permissions. + ScopeSubscription + + // ScopeResourceGroup represents the resource group level permissions. + ScopeResourceGroup +) + // Permissions for Azure. type Permissions struct { Actions []string @@ -37,54 +53,124 @@ type Permissions struct { NotDataActions []string } -// stringsDiff returns the difference between lhs and rhs, i.e. rhs subtracted -// from lhs. -func stringsDiff(lhs, rhs []string) []string { - set := make(map[string]struct{}) - for _, s := range lhs { - set[s] = struct{}{} +// PermissionGroupWithVersion represents a permission group with a specific +// version. +type PermissionGroupWithVersion struct { + Name string + Version int +} + +// Note, permissions must be sorted in alphabetical order. +func (p *Permissions) addPermissions(perm Permissions) { + p.Actions = append(p.Actions, perm.Actions...) + slices.Sort(p.Actions) + p.Actions = slices.Compact(p.Actions) + + p.DataActions = append(p.DataActions, perm.DataActions...) + slices.Sort(p.DataActions) + p.DataActions = slices.Compact(p.DataActions) + + p.NotActions = append(p.NotActions, perm.NotActions...) + slices.Sort(p.NotActions) + p.NotActions = slices.Compact(p.NotActions) + + p.NotDataActions = append(p.NotDataActions, perm.NotDataActions...) + slices.Sort(p.NotDataActions) + p.NotDataActions = slices.Compact(p.NotDataActions) +} + +// Deprecated: Use ScopedPermissions with ScopeLegacy instead. +func (a API) Permissions(ctx context.Context, features []core.Feature) (Permissions, error) { + a.client.Log().Print(log.Trace) + + scopedPerms, err := a.ScopedPermissionsForFeatures(ctx, features) + if err != nil { + return Permissions{}, err + } + + return scopedPerms[ScopeLegacy], nil +} + +// ScopedPermissions returns the permissions and permission groups for the +// specified RSC feature. The Permissions return value always contains three +// items representing the different permission scopes: legacy, subscription and +// resource group. +func (a API) ScopedPermissions(ctx context.Context, feature core.Feature) ([]Permissions, []PermissionGroupWithVersion, error) { + a.client.Log().Print(log.Trace) + + permConfig, err := azure.Wrap(a.client).CloudAccountPermissionConfig(ctx, feature) + if err != nil { + return nil, nil, fmt.Errorf("failed to get permissions: %s", err) } - for _, s := range rhs { - delete(set, s) + scopedPerms := make([]Permissions, 3) + + // Subscription scope. + for _, perm := range permConfig.RolePermissions { + scopedPerms[ScopeSubscription].addPermissions(Permissions{ + Actions: perm.IncludedActions, + DataActions: perm.IncludedDataActions, + NotActions: perm.ExcludedActions, + NotDataActions: perm.ExcludedDataActions, + }) } - diff := make([]string, 0, len(set)) - for s := range set { - diff = append(diff, s) + // Resource group scope. + for _, perm := range permConfig.ResourceGroupRolePermissions { + scopedPerms[ScopeResourceGroup].addPermissions(Permissions{ + Actions: perm.IncludedActions, + DataActions: perm.IncludedDataActions, + NotActions: perm.ExcludedActions, + NotDataActions: perm.ExcludedDataActions, + }) } - return diff + // Legacy scope, provides backwards compatibility with how permissions + // worked before scoped permissions were introduced. + scopedPerms[ScopeLegacy].addPermissions(scopedPerms[ScopeSubscription]) + scopedPerms[ScopeLegacy].addPermissions(scopedPerms[ScopeResourceGroup]) + + // Permission groups. Note, permissions groups must be sorted in + // alphabetical order. + permGroups := make([]PermissionGroupWithVersion, 0, len(permConfig.PermissionGroupVersions)) + for _, permissionGroup := range permConfig.PermissionGroupVersions { + permGroups = append(permGroups, PermissionGroupWithVersion{ + Name: permissionGroup.PermissionGroup, + Version: permissionGroup.Version, + }) + } + slices.SortFunc(permGroups, func(i, j PermissionGroupWithVersion) int { + return cmp.Compare(i.Name, j.Name) + }) + + return scopedPerms, permGroups, nil } -// Permissions returns all Azure permissions required to use the specified RSC -// features. -func (a API) Permissions(ctx context.Context, features []core.Feature) (Permissions, error) { +// ScopedPermissionsForFeatures returns the scoped permissions for a feature +// set. This function violates the RSC Azure permission model and will be +// removed in a future release, use ScopedPermissions instead. +func (a API) ScopedPermissionsForFeatures(ctx context.Context, features []core.Feature) ([]Permissions, error) { a.client.Log().Print(log.Trace) - perms := Permissions{} + scopedPerms := make([]Permissions, 3) for _, feature := range features { - permConfig, err := azure.Wrap(a.client).CloudAccountPermissionConfig(ctx, feature) + scopedPermsForFeature, _, err := a.ScopedPermissions(ctx, feature) if err != nil { - return Permissions{}, fmt.Errorf("failed to get permissions: %v", err) + return nil, err } - - for _, perm := range permConfig.RolePermissions { - perms.Actions = append(perms.Actions, stringsDiff(perm.IncludedActions, perms.Actions)...) - perms.DataActions = append(perms.DataActions, stringsDiff(perm.IncludedDataActions, perms.DataActions)...) - perms.NotActions = append(perms.NotActions, stringsDiff(perm.ExcludedActions, perms.NotActions)...) - perms.NotDataActions = append(perms.NotDataActions, stringsDiff(perm.ExcludedDataActions, perms.NotDataActions)...) + for i := range scopedPerms { + scopedPerms[i].addPermissions(scopedPermsForFeature[i]) } } - return perms, nil + return scopedPerms, nil } // PermissionsUpdated notifies RSC that the permissions for the Azure service // principal for the RSC cloud account with the specified id has been updated. // The permissions should be updated when a feature has the status // StatusMissingPermissions. Updating the permissions is done outside this SDK. -// The features parameter is allowed to be nil. When features is nil all +// The feature parameter is allowed to be nil. When features are nil, all // features are updated. Note that RSC is only notified about features with // status StatusMissingPermissions. func (a API) PermissionsUpdated(ctx context.Context, id IdentityFunc, features []core.Feature) error { @@ -97,7 +183,7 @@ func (a API) PermissionsUpdated(ctx context.Context, id IdentityFunc, features [ account, err := a.Subscription(ctx, id, core.FeatureAll) if err != nil { - return fmt.Errorf("failed to get subscription: %v", err) + return fmt.Errorf("failed to get subscription: %s", err) } for _, feature := range account.Features { @@ -106,14 +192,14 @@ func (a API) PermissionsUpdated(ctx context.Context, id IdentityFunc, features [ } // Check that the feature is in the feature set unless the set is - // empty which is when all features should be updated. + // empty, which is when all features should be updated. if _, ok := featureSet[feature.Name]; len(featureSet) > 0 && !ok { continue } err := azure.Wrap(a.client).UpgradeCloudAccountPermissionsWithoutOAuth(ctx, account.ID, feature.Feature) if err != nil { - return fmt.Errorf("failed to update permissions: %v", err) + return fmt.Errorf("failed to update permissions: %s", err) } } @@ -121,10 +207,10 @@ func (a API) PermissionsUpdated(ctx context.Context, id IdentityFunc, features [ } // PermissionsUpdatedForTenantDomain notifies RSC that the permissions for the -// Azure service principal in a tenant domain has been updated. The permissions +// Azure service principal in a tenant domain have been updated. The permissions // should be updated when a feature has the status StatusMissingPermissions. -// Updating the permissions is done outside the SDK. The features parameter is -// allowed to be nil. When features is nil all features are updated. Note that +// Updating the permissions is done outside the SDK. The feature parameter is +// allowed to be nil. When features are nil, all features are updated. Note that // RSC is only notified about features with status StatusMissingPermissions. func (a API) PermissionsUpdatedForTenantDomain(ctx context.Context, tenantDomain string, features []core.Feature) error { a.client.Log().Print(log.Trace) @@ -136,7 +222,7 @@ func (a API) PermissionsUpdatedForTenantDomain(ctx context.Context, tenantDomain accounts, err := a.Subscriptions(ctx, core.FeatureAll, "") if err != nil { - return fmt.Errorf("failed to get subscriptions: %v", err) + return fmt.Errorf("failed to get subscriptions: %s", err) } for _, account := range accounts { @@ -150,14 +236,14 @@ func (a API) PermissionsUpdatedForTenantDomain(ctx context.Context, tenantDomain } // Check that the feature is in the feature set unless the set is - // empty which is when all features should be updated. + // empty, which is when all features should be updated. if _, ok := featureSet[feature.Name]; len(featureSet) > 0 && !ok { continue } err := azure.Wrap(a.client).UpgradeCloudAccountPermissionsWithoutOAuth(ctx, account.ID, feature.Feature) if err != nil { - return fmt.Errorf("failed to update permissions: %v", err) + return fmt.Errorf("failed to update permissions: %s", err) } } } diff --git a/pkg/polaris/azure/principal.go b/pkg/polaris/azure/principal.go index be3b0ce5..1a724ced 100644 --- a/pkg/polaris/azure/principal.go +++ b/pkg/polaris/azure/principal.go @@ -45,46 +45,32 @@ type servicePrincipal struct { appSecret string tenantID uuid.UUID tenantDomain string - objectID uuid.UUID - rmAuthorizer autorest.Authorizer } // ServicePrincipalFunc returns a service principal initialized from the values // passed to the function creating the ServicePrincipalFunc. type ServicePrincipalFunc func(ctx context.Context) (servicePrincipal, error) -// azureGraph looks up the app display name and object id for the specified +// appDisplayNameFromGraph looks up the app display name for the specified // service principal in Azure AD Graph. -func azureGraph(ctx context.Context, authorizer autorest.Authorizer, principal *servicePrincipal) error { - client := graphrbac.NewServicePrincipalsClient(principal.tenantID.String()) +func appDisplayNameFromGraph(ctx context.Context, authorizer autorest.Authorizer, appID, tenantID uuid.UUID) (string, error) { + client := graphrbac.NewServicePrincipalsClient(tenantID.String()) client.Authorizer = authorizer // This filter should allow the query to run with very few permissions. - filter := fmt.Sprintf("servicePrincipalNames/any(c:c eq '%s')", principal.appID) + filter := fmt.Sprintf("servicePrincipalNames/any(c:c eq '%s')", appID) result, err := client.ListComplete(ctx, filter) if err != nil { - return fmt.Errorf("failed to get Azure service principal names using Graph: %v", err) + return "", fmt.Errorf("failed to get Azure service principal names using Graph: %s", err) } - if !result.NotDone() { - return errors.New("failed to find Azure service principal using Graph") + return "", errors.New("failed to find Azure service principal using Graph") } if result.Value().AppDisplayName == nil { - return errors.New("failed to lookup Azure service principal app display name using Graph") - } - if result.Value().ObjectID == nil { - return errors.New("failed to lookup Azure service principal object id using Graph") - } - - objID, err := uuid.Parse(*result.Value().ObjectID) - if err != nil { - return fmt.Errorf("failed to parse Azure service principal object id: %v", err) + return "", errors.New("failed to lookup Azure service principal app display name using Graph") } - principal.appName = *result.Value().AppDisplayName - principal.objectID = objID - - return nil + return *result.Value().AppDisplayName, nil } // azureServicePrincipalFromAzEnv creates a service principal from the @@ -92,41 +78,35 @@ func azureGraph(ctx context.Context, authorizer autorest.Authorizer, principal * func azurePrincipalFromAzEnv(ctx context.Context, tenantDomain string) (servicePrincipal, error) { graphAuthorizer, err := auth.NewAuthorizerFromEnvironmentWithResource(azure.PublicCloud.GraphEndpoint) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to create Azure Graph authorizer from env: %v", err) - } - - rmAuthorizer, err := auth.NewAuthorizerFromEnvironmentWithResource(azure.PublicCloud.ResourceManagerEndpoint) - if err != nil { - return servicePrincipal{}, - fmt.Errorf("failed to create Azure Resource Manager authorizer from env: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to create Azure Graph authorizer from env: %s", err) } settings, err := auth.GetSettingsFromEnvironment() if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get Azure auth settings from env: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to get Azure auth settings from env: %s", err) } appID, err := uuid.Parse(settings.Values[auth.ClientID]) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse Azure client id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse Azure client id: %s", err) } tenantID, err := uuid.Parse(settings.Values[auth.TenantID]) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse Azure tenant id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse Azure tenant id: %s", err) + } + + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) } principal := servicePrincipal{ appID: appID, + appName: appName, appSecret: settings.Values[auth.ClientSecret], tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, - } - - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - return servicePrincipal{}, - fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) } return principal, nil @@ -137,41 +117,35 @@ func azurePrincipalFromAzEnv(ctx context.Context, tenantDomain string) (serviceP func azurePrincipalFromAzFile(ctx context.Context, tenantDomain string) (servicePrincipal, error) { graphAuthorizer, err := auth.NewAuthorizerFromFile(azure.PublicCloud.GraphEndpoint) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to create Azure Graph authorizer from file: %v", err) - } - - rmAuthorizer, err := auth.NewAuthorizerFromFile(azure.PublicCloud.ResourceManagerEndpoint) - if err != nil { - return servicePrincipal{}, - fmt.Errorf("failed to create Azure Resource Manager authorizer from file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to create Azure Graph authorizer from file: %s", err) } settings, err := auth.GetSettingsFromFile() if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get Azure auth settings from file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to get Azure auth settings from file: %s", err) } appID, err := uuid.Parse(settings.Values[auth.ClientID]) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse Azure client id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse Azure client id: %s", err) } tenantID, err := uuid.Parse(settings.Values[auth.TenantID]) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse Azure tenant id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse Azure tenant id: %s", err) + } + + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) } principal := servicePrincipal{ appID: appID, + appName: appName, appSecret: settings.Values[auth.ClientSecret], tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, - } - - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - return servicePrincipal{}, - fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) } return principal, nil @@ -220,56 +194,48 @@ func decodePrincipalV0(ctx context.Context, data, tenantDomain string) (serviceP var v0 principalV0 if err := decoder.Decode(&v0); err != nil { - return servicePrincipal{}, fmt.Errorf("failed to unmarshal v0 service principal: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to unmarshal v0 service principal: %s", err) } appID, err := uuid.Parse(v0.AppID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %s", err) } tenantID, err := uuid.Parse(v0.TenantID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %s", err) } if tenantDomain != v0.TenantDomain { return servicePrincipal{}, fmt.Errorf("tenant domain mismatch: %s != %s", tenantDomain, v0.TenantDomain) } - rmConfig := auth.NewClientCredentialsConfig(v0.AppID, v0.AppSecret, tenantDomain) - rmAuthorizer, err := rmConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Resource Manager authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } - principal := servicePrincipal{ appID: appID, appName: v0.AppName, appSecret: v0.AppSecret, tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, - } - - graphConfig := auth.ClientCredentialsConfig{ - ClientID: v0.AppID, - ClientSecret: v0.AppSecret, - TenantID: v0.TenantID, - Resource: azure.PublicCloud.GraphEndpoint, - AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, - } - graphAuthorizer, err := graphConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Graph authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} } - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - err = fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } + // If the appName is empty, we try to look up the name using Graph. + if principal.appName == "" { + graphConfig := auth.ClientCredentialsConfig{ + ClientID: v0.AppID, + ClientSecret: v0.AppSecret, + TenantID: v0.TenantID, + Resource: azure.PublicCloud.GraphEndpoint, + AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, + } + graphAuthorizer, err := graphConfig.Authorizer() + if err != nil { + err = fmt.Errorf("failed to get Azure Graph authorizer: %s", err) + return servicePrincipal{}, principalAzureError{err: err} + } - if v0.AppName != "" { - principal.appName = v0.AppName + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) + } + principal.appName = appName } return principal, nil @@ -292,57 +258,48 @@ func decodePrincipalV1(ctx context.Context, data, tenantDomain string) (serviceP var v1 principalV1 if err := decoder.Decode(&v1); err != nil { - return servicePrincipal{}, fmt.Errorf("failed to unmarshal v1 service principal: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to unmarshal v1 service principal: %s", err) } appID, err := uuid.Parse(v1.AppID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %s", err) } tenantID, err := uuid.Parse(v1.TenantID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %s", err) } if tenantDomain != v1.TenantDomain { return servicePrincipal{}, fmt.Errorf("tenant domain mismatch: %s != %s", tenantDomain, v1.TenantDomain) } - rmConfig := auth.NewClientCredentialsConfig(v1.AppID, v1.AppSecret, tenantDomain) - rmAuthorizer, err := rmConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Resource Manager authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } - principal := servicePrincipal{ appID: appID, appName: v1.AppName, appSecret: v1.AppSecret, tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, } - graphConfig := auth.ClientCredentialsConfig{ - ClientID: v1.AppID, - ClientSecret: v1.AppSecret, - TenantID: v1.TenantID, - Resource: azure.PublicCloud.GraphEndpoint, - AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, - } - graphAuthorizer, err := graphConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Graph authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } - - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - err = fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - - } + // If the appName is empty, we try to look up the name using Graph. + if principal.appName == "" { + graphConfig := auth.ClientCredentialsConfig{ + ClientID: v1.AppID, + ClientSecret: v1.AppSecret, + TenantID: v1.TenantID, + Resource: azure.PublicCloud.GraphEndpoint, + AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, + } + graphAuthorizer, err := graphConfig.Authorizer() + if err != nil { + err = fmt.Errorf("failed to get Azure Graph authorizer: %s", err) + return servicePrincipal{}, principalAzureError{err: err} + } - if v1.AppName != "" { - principal.appName = v1.AppName + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) + } + principal.appName = appName } return principal, nil @@ -364,22 +321,15 @@ func decodePrincipalV2(ctx context.Context, data, tenantDomain string) (serviceP var v2 principalV2 if err := decoder.Decode(&v2); err != nil { - return servicePrincipal{}, fmt.Errorf("failed to unmarshal v2 service principal: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to unmarshal v2 service principal: %s", err) } appID, err := uuid.Parse(v2.AppID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to parse service principal app id: %s", err) } tenantID, err := uuid.Parse(v2.TenantID) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %v", err) - } - - rmConfig := auth.NewClientCredentialsConfig(v2.AppID, v2.AppSecret, tenantDomain) - rmAuthorizer, err := rmConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Resource Manager authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} + return servicePrincipal{}, fmt.Errorf("failed to parse service principal tenant id: %s", err) } principal := servicePrincipal{ @@ -388,29 +338,28 @@ func decodePrincipalV2(ctx context.Context, data, tenantDomain string) (serviceP appSecret: v2.AppSecret, tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, } - graphConfig := auth.ClientCredentialsConfig{ - ClientID: v2.AppID, - ClientSecret: v2.AppSecret, - TenantID: v2.TenantID, - Resource: azure.PublicCloud.GraphEndpoint, - AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, - } - graphAuthorizer, err := graphConfig.Authorizer() - if err != nil { - err = fmt.Errorf("failed to get Azure Graph authorizer: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } - - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - err = fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) - return servicePrincipal{}, principalAzureError{err: err} - } + // If the appName is empty, we try to look up the name using Graph. + if principal.appName == "" { + graphConfig := auth.ClientCredentialsConfig{ + ClientID: v2.AppID, + ClientSecret: v2.AppSecret, + TenantID: v2.TenantID, + Resource: azure.PublicCloud.GraphEndpoint, + AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, + } + graphAuthorizer, err := graphConfig.Authorizer() + if err != nil { + err = fmt.Errorf("failed to get Azure Graph authorizer: %s", err) + return servicePrincipal{}, principalAzureError{err: err} + } - if v2.AppName != "" { - principal.appName = v2.AppName + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) + } + principal.appName = appName } return principal, nil @@ -422,7 +371,7 @@ func azurePrincipalFromKeyFile(ctx context.Context, keyFile, tenantDomain string if strings.HasPrefix(keyFile, "~/") { home, err := os.UserHomeDir() if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get home dir: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to get home dir: %s", err) } keyFile = filepath.Join(home, strings.TrimPrefix(keyFile, "~/")) @@ -430,7 +379,7 @@ func azurePrincipalFromKeyFile(ctx context.Context, keyFile, tenantDomain string buf, err := os.ReadFile(keyFile) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to read key file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to read key file: %s", err) } principal, err := decodePrincipalV2(ctx, string(buf), tenantDomain) @@ -457,7 +406,7 @@ func azurePrincipalFromKeyFile(ctx context.Context, keyFile, tenantDomain string return servicePrincipal{}, err } - return servicePrincipal{}, fmt.Errorf("unrecognized file format: %v", keyFile) + return servicePrincipal{}, fmt.Errorf("unrecognized file format: %s", keyFile) } // The Azure SDK requires all parameters be given as environment variables. @@ -478,18 +427,18 @@ func Default(tenantDomain string) ServicePrincipalFunc { case os.Getenv("AZURE_AUTH_LOCATION") != "": principal, err = azurePrincipalFromAzFile(ctx, tenantDomain) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %s", err) } case os.Getenv("AZURE_SERVICEPRINCIPAL_LOCATION") != "": keyfile := os.Getenv("AZURE_SERVICEPRINCIPAL_LOCATION") principal, err = azurePrincipalFromKeyFile(ctx, keyfile, tenantDomain) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %s", err) } default: principal, err = azurePrincipalFromAzEnv(ctx, tenantDomain) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to read service principal from env: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to read service principal from env: %s", err) } } @@ -512,7 +461,7 @@ func SDKAuthFile(authFile, tenantDomain string) ServicePrincipalFunc { if strings.HasPrefix(authFile, "~/") { home, err := os.UserHomeDir() if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get home dir: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to get home dir: %s", err) } authFile = filepath.Join(home, strings.TrimPrefix(authFile, "~/")) @@ -525,12 +474,12 @@ func SDKAuthFile(authFile, tenantDomain string) ServicePrincipalFunc { defer os.Setenv("AZURE_AUTH_LOCATION", authLocation) if err := os.Setenv("AZURE_AUTH_LOCATION", authFile); err != nil { - return servicePrincipal{}, fmt.Errorf("failed to set env AZURE_AUTH_LOCATION: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to set env AZURE_AUTH_LOCATION: %s", err) } principal, err := azurePrincipalFromAzFile(ctx, tenantDomain) if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %v", err) + return servicePrincipal{}, fmt.Errorf("failed to read service principal from file: %s", err) } return principal, nil @@ -538,38 +487,37 @@ func SDKAuthFile(authFile, tenantDomain string) ServicePrincipalFunc { } // ServicePrincipal returns a ServicePrincipalFunc that initializes the service -// principal with the specified values. -func ServicePrincipal(appID uuid.UUID, appSecret string, tenantID uuid.UUID, tenantDomain string) ServicePrincipalFunc { +// principal with the specified values. AppName can be blank, in which case it +// will be looked up using Azure AD Graph. +func ServicePrincipal(appID uuid.UUID, appName string, appSecret string, tenantID uuid.UUID, tenantDomain string) ServicePrincipalFunc { return func(ctx context.Context) (servicePrincipal, error) { - rmConfig := auth.NewClientCredentialsConfig(appID.String(), appSecret, tenantID.String()) - rmAuthorizer, err := rmConfig.Authorizer() - if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get Azure Resource Manager authorizer: %v", err) - } - principal := servicePrincipal{ appID: appID, + appName: appName, appSecret: appSecret, tenantID: tenantID, tenantDomain: tenantDomain, - rmAuthorizer: rmAuthorizer, } - graphConfig := auth.ClientCredentialsConfig{ - ClientID: appID.String(), - ClientSecret: appSecret, - TenantID: tenantID.String(), - Resource: azure.PublicCloud.GraphEndpoint, - AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, - } - graphAuthorizer, err := graphConfig.Authorizer() - if err != nil { - return servicePrincipal{}, fmt.Errorf("failed to get Azure Graph authorizer: %v", err) - } + // If the appName is empty, we try to look up the name using Graph. + if principal.appName == "" { + graphConfig := auth.ClientCredentialsConfig{ + ClientID: appID.String(), + ClientSecret: appSecret, + TenantID: tenantID.String(), + Resource: azure.PublicCloud.GraphEndpoint, + AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint, + } + graphAuthorizer, err := graphConfig.Authorizer() + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to get Azure Graph authorizer: %s", err) + } - if err := azureGraph(ctx, graphAuthorizer, &principal); err != nil { - return servicePrincipal{}, - fmt.Errorf("failed to lookup app display name and object id for service principal: %v", err) + appName, err := appDisplayNameFromGraph(ctx, graphAuthorizer, appID, tenantID) + if err != nil { + return servicePrincipal{}, fmt.Errorf("failed to lookup app display name: %s", err) + } + principal.appName = appName } return principal, nil diff --git a/pkg/polaris/azure/principal_test.go b/pkg/polaris/azure/principal_test.go new file mode 100644 index 00000000..4a8762eb --- /dev/null +++ b/pkg/polaris/azure/principal_test.go @@ -0,0 +1,123 @@ +package azure + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/internal/testsetup" +) + +var ( + appID = uuid.MustParse("4558e8e8-9493-4ece-89cf-1b9ecc026755") + tenantID = uuid.MustParse("ac02265a-440d-42e9-abda-782d8b182d0d") +) + +func TestKeyFile(t *testing.T) { + tt := []struct { + name string + keyFile string + tenantDomain string + }{{ + name: "v0", + keyFile: "testdata/key_file_v0.json", + tenantDomain: "domain.onmicrosoft.com", + }, { + name: "v1", + keyFile: "testdata/key_file_v1.json", + tenantDomain: "domain.onmicrosoft.com", + }, { + name: "v2", + keyFile: "testdata/key_file_v2.json", + tenantDomain: "domain.onmicrosoft.com", + }} + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + principalFunc := KeyFile(tc.keyFile, tc.tenantDomain) + principal, err := principalFunc(context.Background()) + if err != nil { + t.Error(err) + } + + if principal.appID != appID { + t.Errorf("invalid app id: %s", principal.appID) + } + if principal.appName != "App" { + t.Errorf("invalid app name: %s", principal.appName) + } + if principal.appSecret != "secret" { + t.Errorf("invalid app secret: %s", principal.appSecret) + } + if principal.tenantID != tenantID { + t.Errorf("invalid tenant id: %s", principal.tenantID) + } + if principal.tenantDomain != tc.tenantDomain { + t.Errorf("invalid tenant domain: %s", principal.tenantDomain) + } + }) + } +} + +func TestServicePrincipal(t *testing.T) { + principalFunc := ServicePrincipal(appID, "App", "secret", tenantID, "domain.onmicrosoft.com") + principal, err := principalFunc(context.Background()) + if err != nil { + t.Error(err) + } + + if principal.appID != appID { + t.Errorf("invalid app id: %s", principal.appID) + } + if principal.appName != "App" { + t.Errorf("invalid app name: %s", principal.appName) + } + if principal.appSecret != "secret" { + t.Errorf("invalid app secret: %s", principal.appSecret) + } + if principal.tenantID != tenantID { + t.Errorf("invalid tenant id: %s", principal.tenantID) + } + if principal.tenantDomain != "domain.onmicrosoft.com" { + t.Errorf("invalid tenant domain: %s", principal.tenantDomain) + } +} + +// TestServicePrincipalWithoutAppName verifies that app name can be looked up +// from the tenant using the Azure AD Graph API. +func TestServicePrincipalWithoutAppName(t *testing.T) { + if !testsetup.BoolEnvSet("TEST_INTEGRATION") { + t.Skipf("skipping due to env TEST_INTEGRATION not set") + } + + testSub, err := testsetup.AzureSubscription() + if err != nil { + t.Fatal(err) + } + + principalFunc := ServicePrincipal(testSub.PrincipalID, "", testSub.PrincipalSecret, testSub.TenantID, + testSub.TenantDomain) + principal, err := principalFunc(context.Background()) + if err != nil { + t.Error(err) + } + + if principal.appID != testSub.PrincipalID { + t.Errorf("invalid app id: %s", principal.appID) + } + if principal.appName != testSub.PrincipalName { + t.Errorf("invalid app name: %s", principal.appName) + } + if principal.appSecret != testSub.PrincipalSecret { + t.Errorf("invalid app secret: %s", principal.appSecret) + } + if principal.tenantID != testSub.TenantID { + t.Errorf("invalid tenant id: %s", principal.tenantID) + } + if principal.tenantDomain != testSub.TenantDomain { + t.Errorf("invalid tenant domain: %s", principal.tenantDomain) + } +} diff --git a/pkg/polaris/azure/testdata/all_azure_cloud_account_tenants_response.json b/pkg/polaris/azure/testdata/all_azure_cloud_account_tenants_response.json new file mode 100644 index 00000000..40714eb5 --- /dev/null +++ b/pkg/polaris/azure/testdata/all_azure_cloud_account_tenants_response.json @@ -0,0 +1,270 @@ +{ + "data": { + "result": [ + { + "cloudType": "AZUREPUBLICCLOUD", + "azureCloudAccountTenantRubrikId": "ca997d29-1811-4aab-a5dc-649082debe89", + "domainName": "domain1.onmicrosoft.com", + "clientId": "b6a26799-b722-4df6-b2df-9c70433ee55f", + "appName": "app1", + "subscriptionCount": 1, + "subscriptions": [ + { + "id": "f4b69681-2ab8-4edc-81c2-8852e46c1ba3", + "name": "subscription1", + "nativeId": "c263212c-3f26-4b9a-8601-9efb466c8837", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "EASTUS", + "WESTUS" + ], + "resourceGroup": { + "name": "rg1", + "nativeId": "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg1", + "region": "WEST_US", + "tags": [] + } + } + }, + { + "id": "f4b69681-2ab8-4edc-81c2-8852e46c1ba3", + "name": "subscription1", + "nativeId": "c263212c-3f26-4b9a-8601-9efb466c8837", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "CONNECTED", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg2", + "nativeId": "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg2", + "region": "WEST_US", + "tags": [] + } + } + } + ] + }, + { + "cloudType": "AZUREPUBLICCLOUD", + "azureCloudAccountTenantRubrikId": "ca997d29-1811-4aab-a5dc-649082debe89", + "domainName": "domain1.onmicrosoft.com", + "clientId": "b6a26799-b722-4df6-b2df-9c70433ee55f", + "appName": "app1", + "subscriptionCount": 1, + "subscriptions": [ + { + "id": "f4b69681-2ab8-4edc-81c2-8852e46c1ba3", + "name": "subscription1", + "nativeId": "c263212c-3f26-4b9a-8601-9efb466c8837", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "EASTUS", + "WESTUS" + ], + "resourceGroup": { + "name": "rg1", + "nativeId": "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg1", + "region": "WEST_US", + "tags": [] + } + } + }, + { + "id": "f4b69681-2ab8-4edc-81c2-8852e46c1ba3", + "name": "subscription1", + "nativeId": "c263212c-3f26-4b9a-8601-9efb466c8837", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "CONNECTED", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg2", + "nativeId": "/subscriptions/f4b69681-2ab8-4edc-81c2-8852e46c1ba3/resourceGroups/rg2", + "region": "WEST_US", + "tags": [] + } + } + } + ] + }, + { + "cloudType": "AZUREPUBLICCLOUD", + "azureCloudAccountTenantRubrikId": "88af4472-ea52-4c8e-bf05-e4ca581370a7", + "domainName": "domain2.onmicrosoft.com", + "clientId": "6688f45e-b1dc-41d8-b926-3acef4a4beaf", + "appName": "app2", + "subscriptionCount": 2, + "subscriptions": [ + { + "id": "e2e3fb63-2230-4154-9b1b-923f018dbc4f", + "name": "subscription2", + "nativeId": "1ee74f16-10d3-45fe-adfb-7f70ee77f5ee", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS2" + ], + "resourceGroup": { + "name": "rg3", + "nativeId": "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg3", + "region": "WEST_US2", + "tags": [] + } + } + }, + { + "id": "e2e3fb63-2230-4154-9b1b-923f018dbc4f", + "name": "subscription2", + "nativeId": "1ee74f16-10d3-45fe-adfb-7f70ee77f5ee", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "CONNECTED", + "regions": [], + "resourceGroup": { + "name": "rg4", + "nativeId": "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg4", + "region": "WEST_US2", + "tags": [] + } + } + }, + { + "id": "31116cf6-6259-4cfc-b8a6-307cb0744ba1", + "name": "subscription3", + "nativeId": "973bfa00-0bfd-4850-aab1-ebd3f9d9b6b7", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg5", + "nativeId": "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + "region": "WEST_US", + "tags": [ + { + "key": "key1", + "value": "value1" + } + ] + } + } + }, + { + "id": "31116cf6-6259-4cfc-b8a6-307cb0744ba1", + "name": "subscription3", + "nativeId": "973bfa00-0bfd-4850-aab1-ebd3f9d9b6b7", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg5", + "nativeId": "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + "region": "WEST_US", + "tags": [] + } + } + } + ] + }, + { + "cloudType": "AZUREPUBLICCLOUD", + "azureCloudAccountTenantRubrikId": "88af4472-ea52-4c8e-bf05-e4ca581370a7", + "domainName": "domain2.onmicrosoft.com", + "clientId": "6688f45e-b1dc-41d8-b926-3acef4a4beaf", + "appName": "app2", + "subscriptionCount": 2, + "subscriptions": [ + { + "id": "e2e3fb63-2230-4154-9b1b-923f018dbc4f", + "name": "subscription2", + "nativeId": "1ee74f16-10d3-45fe-adfb-7f70ee77f5ee", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS2" + ], + "resourceGroup": { + "name": "rg3", + "nativeId": "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg3", + "region": "WEST_US2", + "tags": [] + } + } + }, + { + "id": "e2e3fb63-2230-4154-9b1b-923f018dbc4f", + "name": "subscription2", + "nativeId": "1ee74f16-10d3-45fe-adfb-7f70ee77f5ee", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "CONNECTED", + "regions": [], + "resourceGroup": { + "name": "rg4", + "nativeId": "/subscriptions/e2e3fb63-2230-4154-9b1b-923f018dbc4f/resourceGroups/rg4", + "region": "WEST_US2", + "tags": [] + } + } + }, + { + "id": "31116cf6-6259-4cfc-b8a6-307cb0744ba1", + "name": "subscription3", + "nativeId": "973bfa00-0bfd-4850-aab1-ebd3f9d9b6b7", + "featureDetail": { + "feature": "CLOUD_NATIVE_PROTECTION", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg5", + "nativeId": "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + "region": "WEST_US", + "tags": [ + { + "key": "key1", + "value": "value1" + } + ] + } + } + }, + { + "id": "31116cf6-6259-4cfc-b8a6-307cb0744ba1", + "name": "subscription3", + "nativeId": "973bfa00-0bfd-4850-aab1-ebd3f9d9b6b7", + "featureDetail": { + "feature": "EXOCOMPUTE", + "status": "MISSING_PERMISSIONS", + "regions": [ + "WESTUS" + ], + "resourceGroup": { + "name": "rg5", + "nativeId": "/subscriptions/31116cf6-6259-4cfc-b8a6-307cb0744ba1/resourceGroups/rg5", + "region": "WEST_US", + "tags": [] + } + } + } + ] + } + ] + } +} diff --git a/pkg/polaris/azure/testdata/key_file_v0.json b/pkg/polaris/azure/testdata/key_file_v0.json new file mode 100644 index 00000000..7fb24a25 --- /dev/null +++ b/pkg/polaris/azure/testdata/key_file_v0.json @@ -0,0 +1,7 @@ +{ + "app_id": "4558e8e8-9493-4ece-89cf-1b9ecc026755", + "app_name": "App", + "app_secret": "secret", + "tenant_id": "ac02265a-440d-42e9-abda-782d8b182d0d", + "tenant_domain":"domain.onmicrosoft.com" +} diff --git a/pkg/polaris/azure/testdata/key_file_v1.json b/pkg/polaris/azure/testdata/key_file_v1.json new file mode 100644 index 00000000..54bc94b3 --- /dev/null +++ b/pkg/polaris/azure/testdata/key_file_v1.json @@ -0,0 +1,7 @@ +{ + "appId": "4558e8e8-9493-4ece-89cf-1b9ecc026755", + "appName": "App", + "appSecret": "secret", + "tenantId": "ac02265a-440d-42e9-abda-782d8b182d0d", + "tenantDomain":"domain.onmicrosoft.com" +} diff --git a/pkg/polaris/azure/testdata/key_file_v2.json b/pkg/polaris/azure/testdata/key_file_v2.json new file mode 100644 index 00000000..7e21c8aa --- /dev/null +++ b/pkg/polaris/azure/testdata/key_file_v2.json @@ -0,0 +1,6 @@ +{ + "appId": "4558e8e8-9493-4ece-89cf-1b9ecc026755", + "appName": "App", + "appSecret": "secret", + "tenantId": "ac02265a-440d-42e9-abda-782d8b182d0d" +} diff --git a/pkg/polaris/config.go b/pkg/polaris/config.go index 203f9925..3f4bfb5e 100644 --- a/pkg/polaris/config.go +++ b/pkg/polaris/config.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "os" "path/filepath" "strings" @@ -31,88 +32,77 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" ) -// UserAccount holds an RSC local user account configuration. Note that RSC -// user accounts with MFA enabled cannot be used. +// UserAccount holds an RSC local user account configuration. Depending on how +// the local user account is stored, the Name field might hold the RSC account +// name. +// +// Note, RSC user accounts with MFA enabled cannot be used. type UserAccount struct { - // Polaris account name. - Name string - - // Polaris account username. - Username string - - // Polaris account password. - Password string + Name string // User account name. + Username string // RSC account username. + Password string // RSC account password. - // Optional Polaris API endpoint. Useful for running the SDK against a test - // service. Defaults to https://{Name}.my.rubrik.com/api. + // Optional RSC API endpoint. Useful for running the SDK against a test + // service. When omitted, it defaults to https://{Name}.my.rubrik.com/api. URL string + accountName string + accountFQDN string + apiURL string envOverride bool + tokenURL string } -func (a *UserAccount) allowEnvOverride() bool { - return a.envOverride +// AccountName returns the RSC account name. Note, this might not be the same +// as the name of the UserAccount. +func (a *UserAccount) AccountName() string { + return a.accountName } -// lookupUserAccount returns a UserAccount from the map of available user -// accounts. If the map contains multiple user accounts and name doesn't match -// any of them, an empty UserAccount with the specified name is returned. -func lookupUserAccount(name string, accounts map[string]UserAccount) UserAccount { - if len(accounts) == 1 { - for name, account := range accounts { - account.Name = name - return account - } - } - if account, ok := accounts[name]; ok { - account.Name = name - return account - } +// AccountFQDN returns the fully qualified domain name of the RSC account. +func (a *UserAccount) AccountFQDN() string { + return a.accountFQDN +} - return UserAccount{Name: name} +// APIURL returns the RSC account API URL. +func (a *UserAccount) APIURL() string { + return a.apiURL } -// userAccountFromEnv returns a UserAccount from the current environment. -func userAccountFromEnv(name string) (UserAccount, error) { - var envKeyFound bool +// TokenURL returns the RSC account token URL. +func (a *UserAccount) TokenURL() string { + return a.tokenURL +} - var accounts map[string]UserAccount - if creds, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_CREDENTIALS"); ok { - if err := json.Unmarshal([]byte(creds), &accounts); err != nil { - return UserAccount{}, fmt.Errorf("failed to unmarshal RUBRIK_POLARIS_ACCOUNT_CREDENTIALS: %s", err) - } - envKeyFound = true - } +func (a *UserAccount) allowEnvOverride() bool { + return a.envOverride +} - if envName, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_NAME"); ok { - name = envName - envKeyFound = true - } - account := lookupUserAccount(name, accounts) - if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_USERNAME"); ok { - account.Username = v - envKeyFound = true - } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_PASSWORD"); ok { - account.Password = v - envKeyFound = true - } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_URL"); ok { - account.URL = v - envKeyFound = true - } +func (a *UserAccount) cacheKeyMaterial() string { + return a.Name + a.URL + a.Username + a.Password +} - if !envKeyFound { - return UserAccount{}, fmt.Errorf("failed to read user account from env: %w", graphql.ErrNotFound) - } +func (a *UserAccount) cacheSuffixMaterial() string { + return a.Name + a.Username +} - return account, nil +// DefaultUserAccount returns a new UserAccount read from the default account +// file. +// +// If allowEnvOverride is true, environment variables can be used to override +// user information in the file. See AccountFromEnv for details. In addition, +// the environment variable RUBRIK_POLARIS_ACCOUNT_FILE can be used to override +// the file that the user information is read from. +// +// Note that RSC user accounts with MFA enabled cannot be used. +func DefaultUserAccount(name string, allowEnvOverride bool) (*UserAccount, error) { + return UserAccountFromFile(DefaultLocalUserFile, name, allowEnvOverride) } // UserAccountFromEnv returns a new UserAccount from the current environment. // The account can be stored as a single JSON encoded environment variable // (RUBRIK_POLARIS_ACCOUNT_CREDENTIALS) or as multiple plain text environment -// variables (e.g. name, username, etc). When using a single environment +// variables (e.g., name, username, etc.). When using a single environment // variable, the JSON content should have the following structure: // // { @@ -142,7 +132,7 @@ func userAccountFromEnv(name string) (UserAccount, error) { // // When using multiple environment variables, they must have the same name as // the public UserAccount fields but be all upper case and prepended with -// RUBRIK_POLARIS_ACCOUNT, e.g. RUBRIK_POLARIS_ACCOUNT_NAME. +// RUBRIK_POLARIS_ACCOUNT, e.g., RUBRIK_POLARIS_ACCOUNT_NAME. // // Note that RSC user accounts with MFA enabled cannot be used. func UserAccountFromEnv() (*UserAccount, error) { @@ -152,49 +142,15 @@ func UserAccountFromEnv() (*UserAccount, error) { } account.envOverride = true - // Validate. - if account.Name == "" { - return nil, errors.New("invalid user account name") - } - if account.Username == "" { - return nil, errors.New("invalid user account username") - } - if account.Password == "" { - return nil, errors.New("invalid user account password") + if err := initUserAccount(&account); err != nil { + return nil, err } return &account, nil } -// userAccountFromFile returns a UserAccount from the specified file with the -// given name. -func userAccountFromFile(file, name string) (UserAccount, error) { - expFile, err := expandPath(file) - if err != nil { - return UserAccount{}, fmt.Errorf("failed to expand file path: %s", err) - } - - buf, err := os.ReadFile(expFile) - if err != nil { - return UserAccount{}, fmt.Errorf("failed to read user account file: %s", err) - } - - var accounts map[string]UserAccount - if err := json.Unmarshal(buf, &accounts); err != nil { - return UserAccount{}, fmt.Errorf("failed to unmarshal user account file: %s", err) - } - - account, ok := accounts[name] - if !ok { - return UserAccount{}, fmt.Errorf("failed to lookup user account %q: %w", name, graphql.ErrNotFound) - } - account.Name = name - - return account, nil -} - // UserAccountFromFile returns a new UserAccount read from the specified file. -// The file must be in the JSON format and the attributes must have the same +// The file must be in the JSON format, and the attributes must have the same // name as the public UserAccount fields but be all lower case. Note that the // name field is used as a key for the JSON object. E.g: // @@ -221,7 +177,7 @@ func userAccountFromFile(file, name string) (UserAccount, error) { // } // // The later format is used to hold multiple accounts. The URL field is -// optional, if it is skipped the URL is constructed from the account name. +// optional, if it is skipped, the URL is constructed from the account name. // // If allowEnvOverride is true, environment variables can be used to override // user information in the file. See AccountFromEnv for details. In addition, @@ -251,7 +207,7 @@ func UserAccountFromFile(file, name string, allowEnvOverride bool) (*UserAccount account, fileErr := userAccountFromFile(file, name) account.envOverride = allowEnvOverride - // Merge with current environment. + // Merge with the current environment. if allowEnvOverride { if envAccount.Name != "" { account.Name = envAccount.Name @@ -268,144 +224,217 @@ func UserAccountFromFile(file, name string, allowEnvOverride bool) (*UserAccount } // Validate. - var msg string - switch { - case account.Name == "": - msg = "invalid user account name" - case account.Username == "": - msg = "invalid user account username" - case account.Password == "": - msg = "invalid user account password" - } - if msg != "" { + if err := initUserAccount(&account); err != nil { if fileErr != nil { - return nil, fmt.Errorf("%s (user account file error: %w)", msg, fileErr) + return nil, fmt.Errorf("%s (user account file error: %w)", err, fileErr) } - return nil, errors.New(msg) + return nil, err } return &account, nil } -// DefaultUserAccount returns a new UserAccount read from the default account -// file. -// -// If allowEnvOverride is true environment variables can be used to override -// user information in the file. See AccountFromEnv for details. In addition, -// the environment variable RUBRIK_POLARIS_ACCOUNT_FILE can be used to override -// the file that the user information is read from. -// -// Note that RSC user accounts with MFA enabled cannot be used. -func DefaultUserAccount(name string, allowEnvOverride bool) (*UserAccount, error) { - return UserAccountFromFile(DefaultLocalUserFile, name, allowEnvOverride) -} - -// ServiceAccount holds a Polaris ServiceAccount configuration. -type ServiceAccount struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - Name string `json:"name"` - AccessTokenURI string `json:"access_token_uri"` - - envOverride bool -} +// lookupUserAccount returns a UserAccount from the map of available user +// accounts. If the map contains multiple user accounts and name doesn't match +// any of them, an empty UserAccount with the specified name is returned. +func lookupUserAccount(name string, accounts map[string]UserAccount) UserAccount { + if len(accounts) == 1 { + for name, account := range accounts { + account.Name = name + return account + } + } + if account, ok := accounts[name]; ok { + account.Name = name + return account + } -func (a *ServiceAccount) allowEnvOverride() bool { - return a.envOverride + return UserAccount{Name: name} } -// serviceAccountFromEnv returns a ServiceAccount from the current environment. -func serviceAccountFromEnv() (ServiceAccount, error) { +// userAccountFromEnv returns a UserAccount from the current environment. +func userAccountFromEnv(name string) (UserAccount, error) { var envKeyFound bool - var account ServiceAccount - if creds, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS"); ok { - if err := json.Unmarshal([]byte(creds), &account); err != nil { - return ServiceAccount{}, fmt.Errorf("failed to unmarshal RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS: %s", err) + var accounts map[string]UserAccount + if creds, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_CREDENTIALS"); ok { + if err := json.Unmarshal([]byte(creds), &accounts); err != nil { + return UserAccount{}, fmt.Errorf("failed to unmarshal RUBRIK_POLARIS_ACCOUNT_CREDENTIALS: %s", err) } envKeyFound = true } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_NAME"); ok { - account.Name = v + if envName, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_NAME"); ok { + name = envName envKeyFound = true } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTID"); ok { - account.ClientID = v + account := lookupUserAccount(name, accounts) + if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_USERNAME"); ok { + account.Username = v envKeyFound = true } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTSECRET"); ok { - account.ClientSecret = v + if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_PASSWORD"); ok { + account.Password = v envKeyFound = true } - if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_ACCESSTOKENURI"); ok { - account.AccessTokenURI = v + if v, ok := os.LookupEnv("RUBRIK_POLARIS_ACCOUNT_URL"); ok { + account.URL = v envKeyFound = true } if !envKeyFound { - return ServiceAccount{}, fmt.Errorf("failed to read service account from env: %w", graphql.ErrNotFound) + return UserAccount{}, fmt.Errorf("failed to read user account from env: %w", graphql.ErrNotFound) } return account, nil } -// ServiceAccountFromEnv returns a new ServiceAccount from the current -// environment. The account can be stored as a single environment variable -// (RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS) or as multiple environment -// variables. When using a single environment variable, the content should be -// the RSC service account file downloaded from RSC when creating the service -// account. When using multiple environment variables, they must have the same -// name as the public ServiceAccount fields but be all upper case and prepended -// with RUBRIK_POLARIS_SERVICEACCOUNT, e.g. RUBRIK_POLARIS_SERVICEACCOUNT_NAME. -func ServiceAccountFromEnv() (*ServiceAccount, error) { - account, err := serviceAccountFromEnv() +// userAccountFromFile returns a UserAccount from the specified file with the +// given name. +func userAccountFromFile(file, name string) (UserAccount, error) { + expFile, err := expandPath(file) if err != nil { - return nil, err + return UserAccount{}, fmt.Errorf("failed to expand file path: %s", err) } - // Validate. - if account.Name == "" { - return nil, errors.New("invalid service account name") - } - if account.ClientID == "" { - return nil, errors.New("invalid service account client id") + buf, err := os.ReadFile(expFile) + if err != nil { + return UserAccount{}, fmt.Errorf("failed to read user account file: %s", err) } - if account.ClientSecret == "" { - return nil, errors.New("invalid service account client secret") + + var accounts map[string]UserAccount + if err := json.Unmarshal(buf, &accounts); err != nil { + return UserAccount{}, fmt.Errorf("failed to unmarshal user account file: %s", err) } - if account.AccessTokenURI == "" { - return nil, errors.New("invalid service account access token uri") + + account, ok := accounts[name] + if !ok { + return UserAccount{}, fmt.Errorf("failed to lookup user account %q: %w", name, graphql.ErrNotFound) } + account.Name = name - return &account, nil + return account, nil } -// serviceAccountFromFile returns a ServiceAccount from the specified RSC -// service account file. -func serviceAccountFromFile(file string) (ServiceAccount, error) { - expFile, err := expandPath(file) +// initUserAccount validates the user account data and initializes the +// additional fields. +func initUserAccount(account *UserAccount) error { + if account.Name == "" { + return errors.New("invalid user account name") + } + if account.Username == "" { + return errors.New("invalid user account username") + } + if account.Password == "" { + return errors.New("invalid user account password") + } + if account.URL == "" { + account.URL = fmt.Sprintf("https://%s.my.rubrik.com/api", account.Name) + } + + // Derive fields. + u, err := url.ParseRequestURI(account.URL) if err != nil { - return ServiceAccount{}, fmt.Errorf("failed to expand file path: %s", err) + return fmt.Errorf("invalid url: %s", err) + } + fqdn := u.Hostname() + i := strings.Index(fqdn, ".") + if i == -1 { + return errors.New("invalid url: no account name found") } + account.accountName = fqdn[:i] + account.accountFQDN = fqdn + account.apiURL = account.URL + account.tokenURL = account.URL + "/session" - buf, err := os.ReadFile(expFile) + return nil +} + +// ServiceAccount holds an RSC ServiceAccount configuration. The Name field +// holds the name of the service account and not the name of the RSC account. +type ServiceAccount struct { + ClientID string `json:"client_id"` // Client ID. + ClientSecret string `json:"client_secret"` // Client secret. + Name string `json:"name"` // Service account name. + AccessTokenURI string `json:"access_token_uri"` // Access token URI. + + accountName string + accountFQDN string + apiURL string + envOverride bool + tokenURL string +} + +// AccountName returns the RSC account name. Note, this might not be the same +// as the name of the ServiceAccount. +func (a *ServiceAccount) AccountName() string { + return a.accountName +} + +// AccountFQDN returns the fully qualified domain name of the RSC account. +func (a *ServiceAccount) AccountFQDN() string { + return a.accountFQDN +} + +// APIURL returns the RSC account API URL. +func (a *ServiceAccount) APIURL() string { + + return a.apiURL +} + +// TokenURL returns the RSC account token URL. +func (a *ServiceAccount) TokenURL() string { + return a.tokenURL +} + +func (a *ServiceAccount) allowEnvOverride() bool { + return a.envOverride +} + +func (a *ServiceAccount) cacheKeyMaterial() string { + return a.Name + a.AccessTokenURI + a.ClientID + a.ClientSecret +} + +func (a *ServiceAccount) cacheSuffixMaterial() string { + return a.Name + a.ClientID +} + +// DefaultServiceAccount returns a new ServiceAccount read from the RSC service +// account file at the default service account location. +// +// If allowEnvOverride is true, environment variables can be used to override +// account information in the file. See ServiceAccountFromEnv for details. In +// addition, the environment variable RUBRIK_POLARIS_SERVICEACCOUNT_FILE can be +// used to override the file that the service account is read from. +func DefaultServiceAccount(allowEnvOverride bool) (*ServiceAccount, error) { + return ServiceAccountFromFile(DefaultServiceAccountFile, allowEnvOverride) +} + +// ServiceAccountFromEnv returns a new ServiceAccount from the current +// environment. The account can be stored as a single environment variable +// (RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS) or as multiple environment +// variables. When using a single environment variable, the content should be +// the RSC service account file downloaded from RSC when creating the service +// account. When using multiple environment variables, they must have the same +// name as the public ServiceAccount fields but be all upper case and prepended +// with RUBRIK_POLARIS_SERVICEACCOUNT, e.g., RUBRIK_POLARIS_SERVICEACCOUNT_NAME. +func ServiceAccountFromEnv() (*ServiceAccount, error) { + account, err := serviceAccountFromEnv() if err != nil { - return ServiceAccount{}, fmt.Errorf("failed to read service account file: %s", err) + return nil, err } - var account ServiceAccount - if err := json.Unmarshal(buf, &account); err != nil { - return ServiceAccount{}, fmt.Errorf("failed to unmarshal service account: %s", err) + if err := initServiceAccount(&account); err != nil { + return nil, err } - return account, nil + return &account, nil } // ServiceAccountFromFile returns a new ServiceAccount read from the specified // RSC service account file. // -// If allowEnvOverride is true environment variables can be used to override +// If allowEnvOverride is true, environment variables can be used to override // account information in the file. See ServiceAccountFromEnv for details. In // addition, the environment variable RUBRIK_POLARIS_SERVICEACCOUNT_FILE can be // used to override the file that the service account is read from. @@ -444,37 +473,108 @@ func ServiceAccountFromFile(file string, allowEnvOverride bool) (*ServiceAccount } } - // Validate. - var msg string - switch { - case account.Name == "": - msg = "invalid service account name" - case account.ClientID == "": - msg = "invalid service account client id" - case account.ClientSecret == "": - msg = "invalid service account client secret" - case account.AccessTokenURI == "": - msg = "invalid service account access token uri" - } - if msg != "" { + if err := initServiceAccount(&account); err != nil { if fileErr != nil { - msg = fmt.Sprintf("%s (service account file error: %s)", msg, fileErr) + return nil, fmt.Errorf("%s (service account file error: %s)", err, fileErr) } - return nil, errors.New(msg) + return nil, err } return &account, nil } -// DefaultServiceAccount returns a new ServiceAccount read from the RSC service -// account file at the default service account location. -// -// If allowEnvOverride is true environment variables can be used to override -// account information in the file. See ServiceAccountFromEnv for details. In -// addition, the environment variable RUBRIK_POLARIS_SERVICEACCOUNT_FILE can be -// used to override the file that the service account is read from. -func DefaultServiceAccount(allowEnvOverride bool) (*ServiceAccount, error) { - return ServiceAccountFromFile(DefaultServiceAccountFile, allowEnvOverride) +// serviceAccountFromEnv returns a ServiceAccount from the current environment. +func serviceAccountFromEnv() (ServiceAccount, error) { + var envKeyFound bool + + var account ServiceAccount + if creds, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS"); ok { + if err := json.Unmarshal([]byte(creds), &account); err != nil { + return ServiceAccount{}, fmt.Errorf("failed to unmarshal RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS: %s", err) + } + envKeyFound = true + } + + if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_NAME"); ok { + account.Name = v + envKeyFound = true + } + if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTID"); ok { + account.ClientID = v + envKeyFound = true + } + if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTSECRET"); ok { + account.ClientSecret = v + envKeyFound = true + } + if v, ok := os.LookupEnv("RUBRIK_POLARIS_SERVICEACCOUNT_ACCESSTOKENURI"); ok { + account.AccessTokenURI = v + envKeyFound = true + } + + if !envKeyFound { + return ServiceAccount{}, fmt.Errorf("failed to read service account from env: %w", graphql.ErrNotFound) + } + + return account, nil +} + +// serviceAccountFromFile returns a ServiceAccount from the specified RSC +// service account file. +func serviceAccountFromFile(file string) (ServiceAccount, error) { + expFile, err := expandPath(file) + if err != nil { + return ServiceAccount{}, fmt.Errorf("failed to expand file path: %s", err) + } + + buf, err := os.ReadFile(expFile) + if err != nil { + return ServiceAccount{}, fmt.Errorf("failed to read service account file: %s", err) + } + + var account ServiceAccount + if err := json.Unmarshal(buf, &account); err != nil { + return ServiceAccount{}, fmt.Errorf("failed to unmarshal service account: %s", err) + } + + return account, nil +} + +// initServiceAccount validates the service account data and initializes the +// additional fields. +func initServiceAccount(account *ServiceAccount) error { + if account.Name == "" { + return errors.New("invalid service account name") + } + if account.ClientID == "" { + return errors.New("invalid service account client id") + } + if account.ClientSecret == "" { + return errors.New("invalid service account client secret") + } + + // Derive account name and FQDN. + u, err := url.ParseRequestURI(account.AccessTokenURI) + if err != nil { + return fmt.Errorf("invalid service account access token uri: %s", err) + } + fqdn := u.Hostname() + i := strings.Index(fqdn, ".") + if i == -1 { + return errors.New("invalid service account access token uri: no account name found") + } + account.accountName = fqdn[:i] + account.accountFQDN = fqdn + + // Derive API URL and token URL. + i = strings.LastIndex(account.AccessTokenURI, "/") + if i < 0 { + return errors.New("invalid service account access token uri: malformed path") + } + account.apiURL = account.AccessTokenURI[:i] + account.tokenURL = account.AccessTokenURI + + return nil } func expandPath(file string) (string, error) { diff --git a/pkg/polaris/config_test.go b/pkg/polaris/config_test.go index 7baf3d7b..54c054c2 100644 --- a/pkg/polaris/config_test.go +++ b/pkg/polaris/config_test.go @@ -85,7 +85,7 @@ func TestSingleUserAccountFromEnvCredentials(t *testing.T) { if account.Password != "password" { t.Errorf("invalid password: %v", account.Password) } - if account.URL != "" { + if account.URL != "https://account.my.rubrik.com/api" { t.Errorf("invalid url: %v", account.URL) } @@ -104,7 +104,7 @@ func TestSingleUserAccountFromEnvCredentials(t *testing.T) { if account.Password != "password" { t.Errorf("invalid password: %v", account.Password) } - if account.URL != "" { + if account.URL != "https://account.my.rubrik.com/api" { t.Errorf("invalid url: %v", account.URL) } @@ -152,7 +152,7 @@ func TestMultipleUserAccountsFromEnvCredentials(t *testing.T) { t.Fatal("name should be required") } - // With correct name from env. + // With the correct name from env. t.Setenv("RUBRIK_POLARIS_ACCOUNT_NAME", "account-2") account, err := UserAccountFromEnv() if err != nil { @@ -167,11 +167,11 @@ func TestMultipleUserAccountsFromEnvCredentials(t *testing.T) { if account.Password != "password-2" { t.Errorf("invalid password: %v", account.Password) } - if account.URL != "" { + if account.URL != "https://account-2.my.rubrik.com/api" { t.Errorf("invalid url: %v", account.URL) } - // With correct name from env and URL. + // With the correct name from env and URL. t.Setenv("RUBRIK_POLARIS_ACCOUNT_CREDENTIALS", `{ "account-1": { "username": "username-1", @@ -212,7 +212,7 @@ func TestDefaultUserAccountFromEnv(t *testing.T) { skipOnEnvs(t, "RUBRIK_POLARIS_ACCOUNT_NAME", "RUBRIK_POLARIS_ACCOUNT_USERNAME", "RUBRIK_POLARIS_ACCOUNT_PASSWORD", "RUBRIK_POLARIS_ACCOUNT_URL") - // If a user account exists in the default location we skip the test. + // If a user account exists in the default location, we skip the test. if _, err := DefaultUserAccount("account", false); err == nil { t.Skip("Default user account exists") } @@ -220,7 +220,7 @@ func TestDefaultUserAccountFromEnv(t *testing.T) { t.Setenv("RUBRIK_POLARIS_ACCOUNT_NAME", "account") t.Setenv("RUBRIK_POLARIS_ACCOUNT_USERNAME", "username") t.Setenv("RUBRIK_POLARIS_ACCOUNT_PASSWORD", "password") - t.Setenv("RUBRIK_POLARIS_ACCOUNT_URL", "url") + t.Setenv("RUBRIK_POLARIS_ACCOUNT_URL", "https://account.my.rubrik.com/api") account, err := DefaultUserAccount("some-account", true) if err != nil { @@ -235,7 +235,7 @@ func TestDefaultUserAccountFromEnv(t *testing.T) { if account.Password != "password" { t.Errorf("invalid password: %v", account.Password) } - if account.URL != "url" { + if account.URL != "https://account.my.rubrik.com/api" { t.Errorf("invalid url: %v", account.URL) } } @@ -243,7 +243,7 @@ func TestDefaultUserAccountFromEnv(t *testing.T) { func TestDefaultUserAccountFromEnvCredentials(t *testing.T) { skipOnEnvs(t, "RUBRIK_POLARIS_ACCOUNT_CREDENTIALS", "RUBRIK_POLARIS_ACCOUNT_NAME") - // If a user account exists in the default location we skip the test. + // If a user account exists in the default location, we skip the test. if _, err := DefaultUserAccount("account", false); err == nil { t.Skip("Default user account exists") } @@ -268,7 +268,7 @@ func TestDefaultUserAccountFromEnvCredentials(t *testing.T) { if account.Password != "password" { t.Errorf("invalid password: %v", account.Password) } - if account.URL != "" { + if account.URL != "https://account.my.rubrik.com/api" { t.Errorf("invalid url: %v", account.URL) } @@ -277,7 +277,7 @@ func TestDefaultUserAccountFromEnvCredentials(t *testing.T) { t.Fatal("no override should require a user account file") } - // Multiple accounts with correct name from a function. + // Multiple accounts with the correct name from a function. t.Setenv("RUBRIK_POLARIS_ACCOUNT_CREDENTIALS", `{ "account-1": { "username": "username-1", @@ -369,7 +369,7 @@ func TestDefaultServiceAccountFromEnv(t *testing.T) { skipOnEnvs(t, "RUBRIK_POLARIS_SERVICEACCOUNT_NAME", "RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTID", "RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTSECRET", "RUBRIK_POLARIS_SERVICEACCOUNT_ACCESSTOKENURI") - // If a service account exists in the default location we skip the test. + // If a service account exists in the default location, we skip the test. if _, err := DefaultServiceAccount(false); err == nil { t.Skip("Default service account exists") } @@ -377,7 +377,7 @@ func TestDefaultServiceAccountFromEnv(t *testing.T) { t.Setenv("RUBRIK_POLARIS_SERVICEACCOUNT_NAME", "account") t.Setenv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTID", "client|id") t.Setenv("RUBRIK_POLARIS_SERVICEACCOUNT_CLIENTSECRET", "secret") - t.Setenv("RUBRIK_POLARIS_SERVICEACCOUNT_ACCESSTOKENURI", "accesstokenuri") + t.Setenv("RUBRIK_POLARIS_SERVICEACCOUNT_ACCESSTOKENURI", "https://account.my.rubrik.com/api/client_token") account, err := DefaultServiceAccount(true) if err != nil { @@ -392,7 +392,7 @@ func TestDefaultServiceAccountFromEnv(t *testing.T) { if account.ClientSecret != "secret" { t.Errorf("invalid client secret: %v", account.ClientSecret) } - if account.AccessTokenURI != "accesstokenuri" { + if account.AccessTokenURI != "https://account.my.rubrik.com/api/client_token" { t.Errorf("invalid access token uri: %v", account.AccessTokenURI) } } @@ -400,7 +400,7 @@ func TestDefaultServiceAccountFromEnv(t *testing.T) { func TestDefaultServiceAccountFromEnvCrendentials(t *testing.T) { skipOnEnvs(t, "RUBRIK_POLARIS_SERVICEACCOUNT_CREDENTIALS") - // If a service account exists in the default location we skip the test. + // If a service account exists in the default location, we skip the test. if _, err := DefaultServiceAccount(false); err == nil { t.Skip("Default service account exists") } diff --git a/pkg/polaris/gcp/gcp.go b/pkg/polaris/gcp/gcp.go index 6e5a53d4..db3d0e31 100644 --- a/pkg/polaris/gcp/gcp.go +++ b/pkg/polaris/gcp/gcp.go @@ -27,7 +27,6 @@ import ( "errors" "fmt" "strconv" - "time" "github.com/google/uuid" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris" @@ -254,7 +253,7 @@ func (a API) AddProject(ctx context.Context, project ProjectFunc, feature core.F a.log.Print(log.Trace) if !feature.Equal(core.FeatureCloudNativeProtection) { - return uuid.Nil, fmt.Errorf("feature not supported on gcp: %v", core.FormatFeature(feature)) + return uuid.Nil, fmt.Errorf("feature not supported on gcp: %v", feature) } if project == nil { @@ -345,12 +344,19 @@ func (a API) RemoveProject(ctx context.Context, id IdentityFunc, feature core.Fe return fmt.Errorf("failed to disable native project: %v", err) } - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) - if err != nil { - return fmt.Errorf("failed to wait for task chain: %v", err) - } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) + if err := core.Wrap(a.client).WaitForFeatureDisableTaskChain(ctx, jobID, func(ctx context.Context) (bool, error) { + account, err := a.Project(ctx, id, feature) + if err != nil { + return false, fmt.Errorf("failed to retrieve status for feature %s: %s", feature, err) + } + + feature, ok := account.Feature(feature) + if !ok { + return false, fmt.Errorf("failed to retrieve status for feature %s: not found", feature) + } + return feature.Status == core.StatusDisabled, nil + }); err != nil { + return fmt.Errorf("failed to wait for task chain %s: %s", jobID, err) } } diff --git a/pkg/polaris/graphql/archival/archival.go b/pkg/polaris/graphql/archival/archival.go new file mode 100644 index 00000000..f578792a --- /dev/null +++ b/pkg/polaris/graphql/archival/archival.go @@ -0,0 +1,179 @@ +//go:generate go run ../queries_gen.go archival + +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package archival + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" +) + +// ListFilter filters target mappings. +type ListFilter interface { + aws.TargetMappingFilter | azure.TargetMappingFilter +} + +// ListResult represents the result of a list operation. +type ListResult[F ListFilter] interface { + ListQuery(filters []F) (string, any) +} + +// ListTargetMappings return all target mappings matching the specified filters. +// In RSC, cloud archival locations are also referred to as target mappings. +func ListTargetMappings[R ListResult[F], F ListFilter](ctx context.Context, gql *graphql.Client, filters []F) ([]R, error) { + gql.Log().Print(log.Trace) + + var result R + query, params := result.ListQuery(filters) + buf, err := gql.Request(ctx, query, params) + if err != nil { + return nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result []R `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, graphql.UnmarshalError(query, err) + } + + return payload.Data.Result, nil +} + +// CreateParams represents the valid type parameters for a create operation. +type CreateParams interface { + aws.StorageSettingCreateParams | azure.StorageSettingCreateParams +} + +// CreateResult represents the result of a create operation. +type CreateResult[P CreateParams] interface { + CreateQuery(cloudAccountID uuid.UUID, createParams P) (string, any) + Validate() (uuid.UUID, error) +} + +// CreateCloudNativeStorageSetting creates a cloud native archival location for +// the specified cloud account. +func CreateCloudNativeStorageSetting[R CreateResult[P], P CreateParams](ctx context.Context, gql *graphql.Client, cloudAccountID uuid.UUID, createParams P) (uuid.UUID, error) { + gql.Log().Print(log.Trace) + + var result R + query, queryParams := result.CreateQuery(cloudAccountID, createParams) + buf, err := gql.Request(ctx, query, queryParams) + if err != nil { + return uuid.Nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result R `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return uuid.Nil, graphql.UnmarshalError(query, err) + } + id, err := payload.Data.Result.Validate() + if err != nil { + return uuid.Nil, graphql.ResponseError(query, err) + } + + return id, nil +} + +// UpdateParams represents the valid type parameters for an update operation. +type UpdateParams interface { + aws.StorageSettingUpdateParams | azure.StorageSettingUpdateParams +} + +// UpdateResult represents the result of an update operation. +type UpdateResult[P UpdateParams] interface { + UpdateQuery(targetMappingID uuid.UUID, updateParams P) (string, any) + Validate() (uuid.UUID, error) +} + +// UpdateCloudNativeStorageSetting updates the cloud native archival location +// with the specified ID. +func UpdateCloudNativeStorageSetting[R UpdateResult[P], P UpdateParams](ctx context.Context, gql *graphql.Client, targetMappingID uuid.UUID, updateParams P) error { + gql.Log().Print(log.Trace) + + var result R + query, queryParams := result.UpdateQuery(targetMappingID, updateParams) + buf, err := gql.Request(ctx, query, queryParams) + if err != nil { + return graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result R `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return graphql.UnmarshalError(query, err) + } + id, err := payload.Data.Result.Validate() + if err != nil { + return graphql.ResponseError(query, err) + } + if id != targetMappingID { + return graphql.ResponseError(query, fmt.Errorf("response ID does not match request ID: %s != %s", id, targetMappingID)) + } + + return nil +} + +// DeleteTargetMapping deletes the target mapping with the specified ID. +// In RSC, cloud archival locations are also referred to as target mappings. +func DeleteTargetMapping(ctx context.Context, gql *graphql.Client, targetMappingID uuid.UUID) error { + gql.Log().Print(log.Trace) + + query := deleteTargetMappingQuery + buf, err := gql.Request(ctx, query, struct { + ID uuid.UUID `json:"id"` + }{ID: targetMappingID}) + if err != nil { + return graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result string `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return graphql.UnmarshalError(query, err) + } + + return nil +} diff --git a/pkg/polaris/graphql/archival/queries.go b/pkg/polaris/graphql/archival/queries.go new file mode 100644 index 00000000..7439f0e7 --- /dev/null +++ b/pkg/polaris/graphql/archival/queries.go @@ -0,0 +1,32 @@ +// Code generated by queries_gen.go DO NOT EDIT. + +// MIT License +// +// Copyright (c) 2021 Rubrik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package archival + +// deleteTargetMapping GraphQL query +var deleteTargetMappingQuery = `mutation SdkGolangDeleteTargetMapping($id: String!) { + result: deleteTargetMapping(input: { + id: $id + }) +}` diff --git a/pkg/polaris/graphql/archival/queries/delete_target_mapping.graphql b/pkg/polaris/graphql/archival/queries/delete_target_mapping.graphql new file mode 100644 index 00000000..747678f7 --- /dev/null +++ b/pkg/polaris/graphql/archival/queries/delete_target_mapping.graphql @@ -0,0 +1,5 @@ +mutation RubrikPolarisSDKRequest($id: String!) { + result: deleteTargetMapping(input: { + id: $id + }) +} diff --git a/pkg/polaris/graphql/aws/archival.go b/pkg/polaris/graphql/aws/archival.go new file mode 100644 index 00000000..00f93669 --- /dev/null +++ b/pkg/polaris/graphql/aws/archival.go @@ -0,0 +1,132 @@ +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package aws + +import "github.com/google/uuid" + +// TargetMappingFilter is used to filter AWS target mappings. Common field +// values are: +// +// - NAME - The name of the target mapping. It can also be used to search for +// a prefix of the name. +// +// - ARCHIVAL_GROUP_TYPE - The type of the archival group, e.g., +// CLOUD_NATIVE_ARCHIVAL_GROUP. +// +// - CLOUD_ACCOUNT_ID - The ID of an RSC cloud account. +// +// - ARCHIVAL_GROUP_ID - The ID of an archival group. Also known as target +// mapping ID. +type TargetMappingFilter struct { + Field string `json:"field"` + Text string `json:"text,omitempty"` + TestList []string `json:"testList,omitempty"` +} + +// TargetMapping represents an AWS cloud archival location. +type TargetMapping struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + GroupType string `json:"groupType"` + TargetType string `json:"targetType"` + ConnectionStatus struct { + Status string `json:"status"` + } `json:"connectionStatus"` + TargetTemplate struct { + CloudAccount struct { + ID uuid.UUID `json:"id"` + } `json:"cloudAccount"` + BucketPrefix string `json:"bucketPrefix"` + StorageClass string `json:"storageClass"` + Region Region `json:"region"` + KMSMasterKey string `json:"kmsMasterKeyId"` + LocTemplate string `json:"cloudNativeLocTemplateType"` + BucketTags []Tag `json:"bucketTags"` + } +} + +func (TargetMapping) ListQuery(filters []TargetMappingFilter) (string, any) { + return allTargetMappingsQuery, append(filters, TargetMappingFilter{ + Field: "ARCHIVAL_LOCATION_TYPE", + Text: "AWS", + }) +} + +// TagsInput holds a list of tags where each tag is a key-value pair. +type TagsInput struct { + TagList []Tag `json:"tagList"` +} + +// StorageSettingCreateParams represents the parameters required to create an +// AWS storage setting. +type StorageSettingCreateParams struct { + Name string `json:"name"` + BucketPrefix string `json:"bucketPrefix"` + StorageClass string `json:"storageClass"` + Region Region `json:"region,omitempty"` + KmsMasterKey string `json:"kmsMasterKeyId"` + LocTemplate string `json:"locTemplateType"` + BucketTags *TagsInput `json:"bucketTags,omitempty"` +} + +// StorageSettingCreateResult represents the result of creating an AWS storage +// setting. +type StorageSettingCreateResult struct { + TargetMapping struct { + ID uuid.UUID `json:"id"` + } `json:"targetMapping"` +} + +func (StorageSettingCreateResult) CreateQuery(cloudAccountID uuid.UUID, createParams StorageSettingCreateParams) (string, any) { + return createCloudNativeAwsStorageSettingQuery, struct { + CloudAccountID uuid.UUID `json:"cloudAccountId"` + StorageSettingCreateParams + }{CloudAccountID: cloudAccountID, StorageSettingCreateParams: createParams} +} + +func (r StorageSettingCreateResult) Validate() (uuid.UUID, error) { + return r.TargetMapping.ID, nil +} + +// StorageSettingUpdateParams represents the parameters required to update an +// AWS storage setting. +type StorageSettingUpdateParams struct { + Name string `json:"name,omitempty"` + StorageClass string `json:"storageClass,omitempty"` + KmsMasterKey string `json:"kmsMasterKeyId,omitempty"` + DeleteAllBucketTags bool `json:"deleteAllBucketTags,omitempty"` + BucketTags *TagsInput `json:"bucketTags,omitempty"` +} + +// StorageSettingUpdateResult represents the result of updating an AWS storage +// setting. +type StorageSettingUpdateResult StorageSettingCreateResult + +func (r StorageSettingUpdateResult) UpdateQuery(targetMappingID uuid.UUID, updateParams StorageSettingUpdateParams) (string, any) { + return updateCloudNativeAwsStorageSettingQuery, struct { + ID uuid.UUID `json:"id"` + StorageSettingUpdateParams + }{ID: targetMappingID, StorageSettingUpdateParams: updateParams} +} + +func (r StorageSettingUpdateResult) Validate() (uuid.UUID, error) { + return r.TargetMapping.ID, nil +} diff --git a/pkg/polaris/graphql/aws/aws.go b/pkg/polaris/graphql/aws/aws.go index facac138..86aa8309 100644 --- a/pkg/polaris/graphql/aws/aws.go +++ b/pkg/polaris/graphql/aws/aws.go @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package aws provides a low level interface to the AWS GraphQL queries +// Package aws provides a low-level interface to the AWS GraphQL queries // provided by the Polaris platform. package aws @@ -67,6 +67,7 @@ type ProtectionFeature string const ( EC2 ProtectionFeature = "EC2" RDS ProtectionFeature = "RDS" + S3 ProtectionFeature = "S3" ) // Region represents an AWS region in Polaris. @@ -103,14 +104,14 @@ const ( RegionUsWest2 Region = "US_WEST_2" ) -// FormatRegion returns the Region as a string formatted in AWS's style, i.e. +// FormatRegion returns the Region as a string formatted in AWS's style, i.e., // lower case and with hyphen as a separator. func FormatRegion(region Region) string { return strings.ReplaceAll(strings.ToLower(string(region)), "_", "-") } // FormatRegions returns the Regions as a slice of strings formatted in AWS's -// style, i.e. lower case and with hyphen as a separator. +// style, i.e., lower case and with hyphen as a separator. func FormatRegions(regions []Region) []string { regs := make([]string, 0, len(regions)) for _, region := range regions { @@ -150,8 +151,7 @@ var validRegions = map[Region]struct{}{ RegionUsWest2: {}, } -// ParseRegion returns the Region matching the given region. Accepts both -// Polaris and AWS style region names. +// Deprecated: use ParseRegionNoValidation. func ParseRegion(region string) (Region, error) { // Polaris region name. r := Region(region) @@ -168,8 +168,7 @@ func ParseRegion(region string) (Region, error) { return RegionUnknown, fmt.Errorf("invalid aws region: %s", region) } -// ParseRegions returns the Regions matching the given regions. Accepts both -// Polaris and AWS style region names. +// Deprecated: use ParseRegionsNoValidation. func ParseRegions(regions []string) ([]Region, error) { regs := make([]Region, 0, len(regions)) @@ -185,7 +184,24 @@ func ParseRegions(regions []string) ([]Region, error) { return regs, nil } -// API wraps around GraphQL clients to give them the RCS AWS API. +// ParseRegionNoValidation returns the Region matching the given region. +// No validation is performed. +func ParseRegionNoValidation(region string) Region { + return Region(strings.ReplaceAll(strings.ToUpper(region), "-", "_")) +} + +// ParseRegionsNoValidation returns the Regions matching the given regions. +// No validation is Performed. +func ParseRegionsNoValidation(regions []string) []Region { + regs := make([]Region, 0, len(regions)) + for _, r := range regions { + regs = append(regs, ParseRegionNoValidation(r)) + } + + return regs +} + +// API wraps around GraphQL client to give it the RSC AWS API. type API struct { Version string // Deprecated: use GQL.DeploymentVersion GQL *graphql.Client @@ -196,3 +212,9 @@ type API struct { func Wrap(gql *graphql.Client) API { return API{GQL: gql, log: gql.Log()} } + +// Tag represents an AWS tag. +type Tag struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/pkg/polaris/graphql/aws/aws_test.go b/pkg/polaris/graphql/aws/aws_test.go index 27fc6b35..3f26ead7 100644 --- a/pkg/polaris/graphql/aws/aws_test.go +++ b/pkg/polaris/graphql/aws/aws_test.go @@ -38,24 +38,12 @@ func TestFormatRegion(t *testing.T) { } func TestParseRegion(t *testing.T) { - region, err := ParseRegion("eu-north-1") - if err != nil { - t.Error(err) - } - if region != RegionEuNorth1 { + if region := ParseRegionNoValidation("eu-north-1"); region != RegionEuNorth1 { t.Errorf("invalid region: %v", region) } - regions, err := ParseRegions([]string{"us-east-1", "us-west-1"}) - if err != nil { - t.Error(err) - } + regions := ParseRegionsNoValidation([]string{"us-east-1", "us-west-1"}) if !reflect.DeepEqual(regions, []Region{RegionUsEast1, RegionUsWest1}) { t.Errorf("invalid region: %v", regions) } - - _, err = ParseRegion("space-moon-1") - if err == nil { - t.Error("expected test to fail") - } } diff --git a/pkg/polaris/graphql/aws/cloud_test.go b/pkg/polaris/graphql/aws/cloud_test.go index 48c0aed9..bcc94a46 100644 --- a/pkg/polaris/graphql/aws/cloud_test.go +++ b/pkg/polaris/graphql/aws/cloud_test.go @@ -40,7 +40,7 @@ func TestValidateAndCreateAWSCloudAccountWithDuplicate(t *testing.T) { // Respond with an error indicating that the account has already been added. srv := testnet.ServeJSONWithStaticToken(lis, func(w http.ResponseWriter, req *http.Request) { - tmpl := template.Must(template.ParseFiles("testdata/validate_and_create_aws_cloud_account.json")) + tmpl := template.Must(template.ParseFiles("testdata/validate_and_create_aws_cloud_account_response.json")) buf, err := io.ReadAll(req.Body) if err != nil { diff --git a/pkg/polaris/graphql/aws/exocompute.go b/pkg/polaris/graphql/aws/exocompute.go index f25a8180..ae98a414 100644 --- a/pkg/polaris/graphql/aws/exocompute.go +++ b/pkg/polaris/graphql/aws/exocompute.go @@ -27,18 +27,27 @@ import ( "fmt" "github.com/google/uuid" - "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -// Subnet represents an AWS subnet. -type Subnet struct { - ID string `json:"subnetId"` - AvailabilityZone string `json:"availabilityZone"` +// ExoConfigsForAccount holds all exocompute configurations for a specific +// account. +type ExoConfigsForAccount struct { + Account CloudAccount `json:"awsCloudAccount"` + Configs []ExoConfig `json:"exocomputeConfigs"` + EligibleRegions []string `json:"exocomputeEligibleRegions"` + Feature Feature `json:"featureDetail"` + MappedAccounts []CloudAccountDetails `json:"mappedCloudAccounts"` } -// ExocomputeConfig represents a single exocompute config. -type ExocomputeConfig struct { +func (r ExoConfigsForAccount) ListQuery(filter string) (string, any) { + return allAwsExocomputeConfigsQuery, struct { + Filter string `json:"awsNativeAccountIdOrNamePrefix"` + }{Filter: filter} +} + +// ExoConfig represents a single exocompute configuration. +type ExoConfig struct { ID string `json:"configUuid"` Region Region `json:"region"` VPCID string `json:"vpcId"` @@ -46,11 +55,11 @@ type ExocomputeConfig struct { Subnet2 Subnet `json:"subnet2"` Message string `json:"message"` - // When true Polaris manages the security groups. + // When true, Polaris manages the security groups. IsManagedByRubrik bool `json:"areSecurityGroupsRscManaged"` - // Security group ids of cluster control plane and worker node. Only needs - // to be specified if IsPolarisManaged is false. + // The security group IDs of the cluster control plane and worker nodes. + // Only needs to be specified if IsPolarisManaged is false. ClusterSecurityGroupID string `json:"clusterSecurityGroupId"` NodeSecurityGroupID string `json:"nodeSecurityGroupId"` @@ -62,16 +71,6 @@ type ExocomputeConfig struct { } } -// ExocomputeConfigsForAccount holds all exocompute configs for a specific -// account. -type ExocomputeConfigsForAccount struct { - Account CloudAccount `json:"awsCloudAccount"` - Configs []ExocomputeConfig `json:"exocomputeConfigs"` - EligibleRegions []string `json:"exocomputeEligibleRegions"` - Feature Feature `json:"featureDetail"` - MappedAccounts []CloudAccountDetails `json:"mappedCloudAccounts"` -} - // CloudAccountDetails holds the details about an exocompute application account // mapping. type CloudAccountDetails struct { @@ -80,148 +79,150 @@ type CloudAccountDetails struct { Name string `json:"name"` } -// ExocomputeConfigs returns all exocompute configs matching the specified -// filter. The filter can be used to search for account name or account id. -func (a API) ExocomputeConfigs(ctx context.Context, filter string) ([]ExocomputeConfigsForAccount, error) { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, allAwsExocomputeConfigsQuery, struct { - Filter string `json:"awsNativeAccountIdOrNamePrefix"` - }{Filter: filter}) - if err != nil { - return nil, fmt.Errorf("failed to request allAwsExocomputeConfigs: %w", err) - } - a.log.Printf(log.Debug, "allAwsExocomputeConfigs(%q): %s", filter, string(buf)) - - var payload struct { - Data struct { - Result []ExocomputeConfigsForAccount `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return nil, fmt.Errorf("failed to unmarshal allAwsExocomputeConfigs: %v", err) - } - - return payload.Data.Result, nil +// Subnet represents an AWS subnet. +type Subnet struct { + ID string `json:"subnetId"` + AvailabilityZone string `json:"availabilityZone"` } -// ExocomputeConfigCreate represents an exocompute config to be created by -// Polaris. -type ExocomputeConfigCreate struct { +// ExoCreateParams represents the parameters required to create an AWS +// exocompute configuration. +type ExoCreateParams struct { Region Region `json:"region"` // Only required for RSC managed clusters VPCID string `json:"vpcId,omitempty"` Subnets []Subnet `json:"subnets,omitempty"` - // When true Rubrik will manage the security groups. + // When true, Rubrik will manage the security groups. IsManagedByRubrik bool `json:"isRscManaged"` - // Security group ids of cluster control plane and worker node. Only needs - // to be specified if IsPolarisManaged is false. - ClusterSecurityGroupId string `json:"clusterSecurityGroupId"` - NodeSecurityGroupId string `json:"nodeSecurityGroupId"` + // The security group IDs of the cluster control plane and worker nodes. + // Only needs to be specified if IsPolarisManaged is false. + ClusterSecurityGroupId string `json:"clusterSecurityGroupId,omitempty"` + NodeSecurityGroupId string `json:"nodeSecurityGroupId,omitempty"` } -// CreateExocomputeConfig creates a new exocompute config for the account with -// the specified RSC cloud account id. Returns the created exocompute config -func (a API) CreateExocomputeConfig(ctx context.Context, cloudAccountID uuid.UUID, config ExocomputeConfigCreate) (ExocomputeConfig, error) { - a.log.Print(log.Trace) +// ExoCreateResult represents the result of creating an AWS exocompute +// configuration. +type ExoCreateResult struct { + Configs []ExoConfig `json:"exocomputeConfigs"` +} - buf, err := a.GQL.Request(ctx, createAwsExocomputeConfigsQuery, struct { - ID uuid.UUID `json:"cloudAccountId"` - Configs []ExocomputeConfigCreate `json:"configs"` - }{ID: cloudAccountID, Configs: []ExocomputeConfigCreate{config}}) - if err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to request createAwsExocomputeConfigs: %w", err) - } - a.log.Printf(log.Debug, "createAwsExocomputeConfigs(%q, %v): %s", cloudAccountID, config, string(buf)) +func (r ExoCreateResult) CreateQuery(cloudAccountID uuid.UUID, createParams ExoCreateParams) (string, any) { + return createAwsExocomputeConfigsQuery, struct { + ID uuid.UUID `json:"cloudAccountId"` + Configs []ExoCreateParams `json:"configs"` + }{ID: cloudAccountID, Configs: []ExoCreateParams{createParams}} +} - var payload struct { - Data struct { - Query struct { - Configs []ExocomputeConfig `json:"configs"` - } `json:"createAwsExocomputeConfigs"` - } `json:"data"` +func (r ExoCreateResult) Validate() (uuid.UUID, error) { + if len(r.Configs) != 1 { + return uuid.Nil, errors.New("expected a single create result") } - if err := json.Unmarshal(buf, &payload); err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to unmarshal createAwsExocomputeConfigs: %v", err) - } - if len(payload.Data.Query.Configs) != 1 { - return ExocomputeConfig{}, errors.New("expected a single result") + if msg := r.Configs[0].Message; msg != "" { + return uuid.Nil, errors.New(msg) } - if payload.Data.Query.Configs[0].Message != "" { - return ExocomputeConfig{}, errors.New(payload.Data.Query.Configs[0].Message) + id, err := uuid.Parse(r.Configs[0].ID) + if err != nil { + return uuid.Nil, err } - return payload.Data.Query.Configs[0], nil + return id, nil } -// UpdateExocomputeConfig updates an exocompute config for the account with -// the specified RSC cloud account id. Returns the updated exocompute config. -func (a API) UpdateExocomputeConfig(ctx context.Context, cloudAccountID uuid.UUID, config ExocomputeConfigCreate) (ExocomputeConfig, error) { - a.log.Print(log.Trace) +// ExoUpdateParams represents the parameters required to update an AWS +// exocompute configuration. +type ExoUpdateParams ExoCreateParams - buf, err := a.GQL.Request(ctx, updateAwsExocomputeConfigsQuery, struct { - ID uuid.UUID `json:"cloudAccountId"` - Configs []ExocomputeConfigCreate `json:"configs"` - }{ID: cloudAccountID, Configs: []ExocomputeConfigCreate{config}}) - if err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to request updateAwsExocomputeConfigs: %w", err) - } - a.log.Printf(log.Debug, "updateAwsExocomputeConfigs(%q, %v): %s", cloudAccountID, config, string(buf)) +// ExoUpdateResult represents the result of updating an AWS exocompute +// configuration. +type ExoUpdateResult ExoCreateResult - var payload struct { - Data struct { - Query struct { - Configs []ExocomputeConfig `json:"configs"` - } `json:"updateAwsExocomputeConfigs"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to unmarshal updateAwsExocomputeConfigs: %v", err) +func (r ExoUpdateResult) UpdateQuery(cloudAccountID uuid.UUID, updateParams ExoUpdateParams) (string, any) { + return updateAwsExocomputeConfigsQuery, struct { + ID uuid.UUID `json:"cloudAccountId"` + Config ExoUpdateParams `json:"configs"` + }{ID: cloudAccountID, Config: updateParams} +} + +func (r ExoUpdateResult) Validate() (uuid.UUID, error) { + if len(r.Configs) != 1 { + return uuid.Nil, errors.New("expected a single update result") } - if len(payload.Data.Query.Configs) != 1 { - return ExocomputeConfig{}, errors.New("expected a single result") + if msg := r.Configs[0].Message; msg != "" { + return uuid.Nil, errors.New(msg) } - if payload.Data.Query.Configs[0].Message != "" { - return ExocomputeConfig{}, errors.New(payload.Data.Query.Configs[0].Message) + id, err := uuid.Parse(r.Configs[0].ID) + if err != nil { + return uuid.Nil, err } - return payload.Data.Query.Configs[0], nil + return id, nil } -// DeleteExocomputeConfig deletes the exocompute config with the specified RSC -// exocompute config id. -func (a API) DeleteExocomputeConfig(ctx context.Context, configID uuid.UUID) error { - a.log.Print(log.Trace) +// ExoDeleteResult represents the result of deleting an AWS exocompute +// configuration. +type ExoDeleteResult struct { + Status []struct { + ID uuid.UUID `json:"exocomputeConfigId"` + Success bool `json:"success"` + } `json:"deletionStatus"` +} - buf, err := a.GQL.Request(ctx, deleteAwsExocomputeConfigsQuery, struct { +func (r ExoDeleteResult) DeleteQuery(configID uuid.UUID) (string, any) { + return deleteAwsExocomputeConfigsQuery, struct { IDs []uuid.UUID `json:"configIdsToBeDeleted"` - }{IDs: []uuid.UUID{configID}}) - if err != nil { - return fmt.Errorf("failed to request deleteAwsExocomputeConfigs: %w", err) - } - a.log.Printf(log.Debug, "deleteAwsExocomputeConfigs(%q): %s", configID, string(buf)) + }{IDs: []uuid.UUID{configID}} +} - var payload struct { - Data struct { - Query struct { - Status []struct { - ID uuid.UUID `json:"exocomputeConfigId"` - Success bool `json:"success"` - } `json:"deletionStatus"` - } `json:"deleteAwsExocomputeConfigs"` - } `json:"data"` +func (r ExoDeleteResult) Validate() (uuid.UUID, error) { + if len(r.Status) != 1 { + return uuid.Nil, errors.New("expected a single delete result") } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal deleteAwsExocomputeConfigs: %v", err) + if !r.Status[0].Success { + return uuid.Nil, errors.New("failed to delete exocompute config") } - if len(payload.Data.Query.Status) != 1 { - return errors.New("expected a single result") + + return r.Status[0].ID, nil +} + +// ExoMapResult represents the result of mapping an AWS application cloud +// account to an AWS host cloud account. +type ExoMapResult struct { + Success bool `json:"isSuccess"` +} + +func (r ExoMapResult) MapQuery(hostCloudAccountID, appCloudAccountID uuid.UUID) (string, any) { + return mapCloudAccountExocomputeAccountQuery, struct { + HostCloudAccountID uuid.UUID `json:"exocomputeCloudAccountId"` + AppCloudAccountIDs []uuid.UUID `json:"cloudAccountIds"` + }{HostCloudAccountID: hostCloudAccountID, AppCloudAccountIDs: []uuid.UUID{appCloudAccountID}} +} + +func (r ExoMapResult) Validate() error { + if !r.Success { + return errors.New("failed to map application cloud account") } - if !payload.Data.Query.Status[0].Success { - return errors.New("delete exocompute config failed") + + return nil +} + +// ExoUnmapResult represents the result of unmapping an AWS application cloud +// account. +type ExoUnmapResult struct { + Success bool `json:"isSuccess"` +} + +func (r ExoUnmapResult) UnmapQuery(appCloudAccountID uuid.UUID) (string, any) { + return unmapCloudAccountExocomputeAccountQuery, struct { + AppCloudAccountIDs []uuid.UUID `json:"cloudAccountIds"` + }{AppCloudAccountIDs: []uuid.UUID{appCloudAccountID}} +} + +func (r ExoUnmapResult) Validate() error { + if !r.Success { + return errors.New("failed to unmap application cloud account") } return nil @@ -259,70 +260,11 @@ func (a API) StartExocomputeDisableJob(ctx context.Context, nativeID uuid.UUID) return payload.Data.Result.JobID, nil } -// MapCloudAccountExocomputeAccount maps the slice of exocompute application -// accounts to the specified exocompute host account. -func (a API) MapCloudAccountExocomputeAccount(ctx context.Context, hostID uuid.UUID, appIDs []uuid.UUID) error { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, mapCloudAccountExocomputeAccountQuery, struct { - HostID uuid.UUID `json:"exocomputeCloudAccountId"` - AppIDs []uuid.UUID `json:"cloudAccountIds"` - }{HostID: hostID, AppIDs: appIDs}) - if err != nil { - return fmt.Errorf("failed to request mapCloudAccountExocomputeAccount: %w", err) - } - a.log.Printf(log.Debug, "mapCloudAccountExocomputeAccount(%q, %v): %s", hostID, appIDs, string(buf)) - - var payload struct { - Data struct { - Result struct { - Success bool `json:"isSuccess"` - } `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal mapCloudAccountExocomputeAccount: %v", err) - } - if !payload.Data.Result.Success { - return errors.New("") - } - - return nil -} - -// UnmapCloudAccountExocomputeAccount unmaps the slice of exocompute application -// accounts. -func (a API) UnmapCloudAccountExocomputeAccount(ctx context.Context, appIDs []uuid.UUID) error { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, unmapCloudAccountExocomputeAccountQuery, struct { - AppIDs []uuid.UUID `json:"cloudAccountIds"` - }{AppIDs: appIDs}) - if err != nil { - return fmt.Errorf("failed to request unmapCloudAccountExocomputeAccount: %w", err) - } - a.log.Printf(log.Debug, "unmapCloudAccountExocomputeAccount(%v): %s", appIDs, string(buf)) - - var payload struct { - Data struct { - Result struct { - Success bool `json:"isSuccess"` - } `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal unmapCloudAccountExocomputeAccount: %v", err) - } - if !payload.Data.Result.Success { - return errors.New("") - } - - return nil -} - -// ConnectExocomputeCluster connects the named cluster to specified exocompute -// configration. The cluster ID and connection command are returned. -func (a API) ConnectExocomputeCluster(ctx context.Context, configID uuid.UUID, clusterName string) (uuid.UUID, string, error) { +// ConnectExocomputeCluster connects the named cluster to a specified exocompute +// configuration. The cluster ID and two different ways to connect the cluster +// are returned. The first way to connect the cluster is the kubectl connection +// command, and the second way is the k8s spec (YAML). +func (a API) ConnectExocomputeCluster(ctx context.Context, configID uuid.UUID, clusterName string) (uuid.UUID, string, string, error) { a.log.Print(log.Trace) buf, err := a.GQL.Request(ctx, awsExocomputeClusterConnectQuery, struct { @@ -330,26 +272,27 @@ func (a API) ConnectExocomputeCluster(ctx context.Context, configID uuid.UUID, c ClusterName string `json:"clusterName"` }{ConfigID: configID, ClusterName: clusterName}) if err != nil { - return uuid.Nil, "", fmt.Errorf("failed to request awsExocomputeClusterConnect: %w", err) + return uuid.Nil, "", "", fmt.Errorf("failed to request awsExocomputeClusterConnect: %w", err) } a.log.Printf(log.Debug, "awsExocomputeClusterConnect(%q, %q): %s", configID, clusterName, string(buf)) var payload struct { Data struct { Result struct { - ID uuid.UUID `json:"clusterUuid"` - Command string `json:"connectionCommand"` + ID uuid.UUID `json:"clusterUuid"` + Command string `json:"connectionCommand"` + SetupYAML string `json:"clusterSetupYaml"` } `json:"result"` } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return uuid.Nil, "", fmt.Errorf("failed to unmarshal awsExocomputeClusterConnect: %v", err) + return uuid.Nil, "", "", fmt.Errorf("failed to unmarshal awsExocomputeClusterConnect: %v", err) } - return payload.Data.Result.ID, payload.Data.Result.Command, nil + return payload.Data.Result.ID, payload.Data.Result.Command, payload.Data.Result.SetupYAML, nil } -// DisconnectExocomputeCluster disconnects the exocomptue cluster with the +// DisconnectExocomputeCluster disconnects the exocompute cluster with the // specified ID from RSC. func (a API) DisconnectExocomputeCluster(ctx context.Context, clusterID uuid.UUID) error { a.log.Print(log.Trace) diff --git a/pkg/polaris/graphql/aws/queries.go b/pkg/polaris/graphql/aws/queries.go index d41d9cb4..4be7d687 100644 --- a/pkg/polaris/graphql/aws/queries.go +++ b/pkg/polaris/graphql/aws/queries.go @@ -76,9 +76,7 @@ var allAwsExocomputeConfigsQuery = `query SdkGolangAllAwsExocomputeConfigs($awsN taskchainId } region - ... on AwsCustomerManagedExocomputeConfig { - clusterName - } + message ... on AwsRscManagedExocomputeConfig { vpcId clusterSecurityGroupId @@ -203,6 +201,7 @@ var awsExocomputeClusterConnectQuery = `mutation SdkGolangAwsExocomputeClusterCo clusterName: $clusterName, exocomputeConfigId: $exocomputeConfigId }) { + clusterSetupYaml clusterUuid connectionCommand } @@ -287,23 +286,31 @@ var bulkDeleteAwsCloudAccountWithoutCftQuery = `mutation SdkGolangBulkDeleteAwsC // createAwsExocomputeConfigs GraphQL query var createAwsExocomputeConfigsQuery = `mutation SdkGolangCreateAwsExocomputeConfigs($cloudAccountId: UUID!, $configs: [AwsExocomputeConfigInput!]!) { - createAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { - configs { - areSecurityGroupsRscManaged - clusterSecurityGroupId + result: createAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { + exocomputeConfigs { configUuid - message - nodeSecurityGroupId - region - subnet1 { - availabilityZone - subnetId + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId } - subnet2 { - availabilityZone - subnetId + region + message + ... on AwsRscManagedExocomputeConfig { + vpcId + clusterSecurityGroupId + nodeSecurityGroupId + subnet1 { + availabilityZone + subnetId + } + subnet2 { + availabilityZone + subnetId + } + areSecurityGroupsRscManaged } - vpcId } } }` @@ -337,7 +344,7 @@ var createCloudNativeAwsStorageSettingQuery = `mutation SdkGolangCreateCloudNati // deleteAwsExocomputeConfigs GraphQL query var deleteAwsExocomputeConfigsQuery = `mutation SdkGolangDeleteAwsExocomputeConfigs($configIdsToBeDeleted: [UUID!]!) { - deleteAwsExocomputeConfigs(input: {configIdsToBeDeleted: $configIdsToBeDeleted}) { + result: deleteAwsExocomputeConfigs(input: {configIdsToBeDeleted: $configIdsToBeDeleted}) { deletionStatus { exocomputeConfigId success @@ -354,7 +361,7 @@ var deleteTargetMappingQuery = `mutation SdkGolangDeleteTargetMapping($id: Strin // disconnectAwsExocomputeCluster GraphQL query var disconnectAwsExocomputeClusterQuery = `mutation SdkGolangDisconnectAwsExocomputeCluster($clusterId: UUID!) { - disconnectAwsExocomputeCluster(input: { + result: disconnectAwsExocomputeCluster(input: { clusterId: $clusterId }) }` @@ -498,39 +505,51 @@ var updateAwsCloudAccountFeatureQuery = `mutation SdkGolangUpdateAwsCloudAccount // updateAwsExocomputeConfigs GraphQL query var updateAwsExocomputeConfigsQuery = `mutation SdkGolangUpdateAwsExocomputeConfigs($cloudAccountId: UUID!, $configs: [AwsExocomputeConfigInput!]!) { - updateAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { - configs { - areSecurityGroupsRscManaged - clusterSecurityGroupId + result: updateAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { + exocomputeConfigs { configUuid - message - nodeSecurityGroupId - region - subnet1 { - availabilityZone - subnetId + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId } - subnet2 { - availabilityZone - subnetId + region + message + ... on AwsRscManagedExocomputeConfig { + vpcId + clusterSecurityGroupId + nodeSecurityGroupId + subnet1 { + availabilityZone + subnetId + } + subnet2 { + availabilityZone + subnetId + } + areSecurityGroupsRscManaged } - vpcId } } }` // updateCloudNativeAwsStorageSetting GraphQL query var updateCloudNativeAwsStorageSettingQuery = `mutation SdkGolangUpdateCloudNativeAwsStorageSetting( - $id: UUID!, - $name: String, - $storageClass: AwsStorageClass, - $kmsMasterKeyId: String + $id: UUID!, + $name: String, + $storageClass: AwsStorageClass, + $kmsMasterKeyId: String, + $deleteAllBucketTags: Boolean + $bucketTags: TagsInput, ) { result: updateCloudNativeAwsStorageSetting(input: { - id: $id, - name: $name, - storageClass: $storageClass, - kmsMasterKeyId: $kmsMasterKeyId + id: $id, + name: $name, + storageClass: $storageClass, + kmsMasterKeyId: $kmsMasterKeyId, + deleteAllBucketTags: $deleteAllBucketTags + bucketTags: $bucketTags, }) { targetMapping { id diff --git a/pkg/polaris/graphql/aws/queries/all_aws_exocompute_configs.graphql b/pkg/polaris/graphql/aws/queries/all_aws_exocompute_configs.graphql index 3fcaa436..3ceb2cfc 100644 --- a/pkg/polaris/graphql/aws/queries/all_aws_exocompute_configs.graphql +++ b/pkg/polaris/graphql/aws/queries/all_aws_exocompute_configs.graphql @@ -28,9 +28,7 @@ query RubrikPolarisSDKRequest($awsNativeAccountIdOrNamePrefix: String!) { taskchainId } region - ... on AwsCustomerManagedExocomputeConfig { - clusterName - } + message ... on AwsRscManagedExocomputeConfig { vpcId clusterSecurityGroupId diff --git a/pkg/polaris/graphql/aws/queries/aws_exocompute_cluster_connect.graphql b/pkg/polaris/graphql/aws/queries/aws_exocompute_cluster_connect.graphql index f56aee94..2466b14c 100644 --- a/pkg/polaris/graphql/aws/queries/aws_exocompute_cluster_connect.graphql +++ b/pkg/polaris/graphql/aws/queries/aws_exocompute_cluster_connect.graphql @@ -3,6 +3,7 @@ mutation RubrikPolarisSDKRequest($clusterName: String!, $exocomputeConfigId: UUI clusterName: $clusterName, exocomputeConfigId: $exocomputeConfigId }) { + clusterSetupYaml clusterUuid connectionCommand } diff --git a/pkg/polaris/graphql/aws/queries/create_aws_exocompute_configs.graphql b/pkg/polaris/graphql/aws/queries/create_aws_exocompute_configs.graphql index a8a9a628..ea2cc92a 100644 --- a/pkg/polaris/graphql/aws/queries/create_aws_exocompute_configs.graphql +++ b/pkg/polaris/graphql/aws/queries/create_aws_exocompute_configs.graphql @@ -1,21 +1,29 @@ mutation RubrikPolarisSDKRequest($cloudAccountId: UUID!, $configs: [AwsExocomputeConfigInput!]!) { - createAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { - configs { - areSecurityGroupsRscManaged - clusterSecurityGroupId + result: createAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { + exocomputeConfigs { configUuid - message - nodeSecurityGroupId - region - subnet1 { - availabilityZone - subnetId + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId } - subnet2 { - availabilityZone - subnetId + region + message + ... on AwsRscManagedExocomputeConfig { + vpcId + clusterSecurityGroupId + nodeSecurityGroupId + subnet1 { + availabilityZone + subnetId + } + subnet2 { + availabilityZone + subnetId + } + areSecurityGroupsRscManaged } - vpcId } } } diff --git a/pkg/polaris/graphql/aws/queries/delete_aws_exocompute_configs.graphql b/pkg/polaris/graphql/aws/queries/delete_aws_exocompute_configs.graphql index 3fb618d6..9edc5890 100644 --- a/pkg/polaris/graphql/aws/queries/delete_aws_exocompute_configs.graphql +++ b/pkg/polaris/graphql/aws/queries/delete_aws_exocompute_configs.graphql @@ -1,5 +1,5 @@ mutation RubrikPolarisSDKRequest($configIdsToBeDeleted: [UUID!]!) { - deleteAwsExocomputeConfigs(input: {configIdsToBeDeleted: $configIdsToBeDeleted}) { + result: deleteAwsExocomputeConfigs(input: {configIdsToBeDeleted: $configIdsToBeDeleted}) { deletionStatus { exocomputeConfigId success diff --git a/pkg/polaris/graphql/aws/queries/disconnect_aws_exocompute_cluster.graphql b/pkg/polaris/graphql/aws/queries/disconnect_aws_exocompute_cluster.graphql index 65ce19ec..4e4ea258 100644 --- a/pkg/polaris/graphql/aws/queries/disconnect_aws_exocompute_cluster.graphql +++ b/pkg/polaris/graphql/aws/queries/disconnect_aws_exocompute_cluster.graphql @@ -1,5 +1,5 @@ mutation RubrikPolarisSDKRequest($clusterId: UUID!) { - disconnectAwsExocomputeCluster(input: { + result: disconnectAwsExocomputeCluster(input: { clusterId: $clusterId }) } diff --git a/pkg/polaris/graphql/aws/queries/update_aws_exocompute_configs.graphql b/pkg/polaris/graphql/aws/queries/update_aws_exocompute_configs.graphql index 1a4e13ec..7a9cfab4 100644 --- a/pkg/polaris/graphql/aws/queries/update_aws_exocompute_configs.graphql +++ b/pkg/polaris/graphql/aws/queries/update_aws_exocompute_configs.graphql @@ -1,21 +1,29 @@ mutation RubrikPolarisSDKRequest($cloudAccountId: UUID!, $configs: [AwsExocomputeConfigInput!]!) { - updateAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { - configs { - areSecurityGroupsRscManaged - clusterSecurityGroupId + result: updateAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { + exocomputeConfigs { configUuid - message - nodeSecurityGroupId - region - subnet1 { - availabilityZone - subnetId + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId } - subnet2 { - availabilityZone - subnetId + region + message + ... on AwsRscManagedExocomputeConfig { + vpcId + clusterSecurityGroupId + nodeSecurityGroupId + subnet1 { + availabilityZone + subnetId + } + subnet2 { + availabilityZone + subnetId + } + areSecurityGroupsRscManaged } - vpcId } } } diff --git a/pkg/polaris/graphql/aws/queries/update_cloud_native_aws_storage_setting.graphql b/pkg/polaris/graphql/aws/queries/update_cloud_native_aws_storage_setting.graphql index 9d4da4af..06dbeac3 100644 --- a/pkg/polaris/graphql/aws/queries/update_cloud_native_aws_storage_setting.graphql +++ b/pkg/polaris/graphql/aws/queries/update_cloud_native_aws_storage_setting.graphql @@ -1,14 +1,18 @@ mutation RubrikPolarisSDKRequest( - $id: UUID!, - $name: String, - $storageClass: AwsStorageClass, - $kmsMasterKeyId: String + $id: UUID!, + $name: String, + $storageClass: AwsStorageClass, + $kmsMasterKeyId: String, + $deleteAllBucketTags: Boolean + $bucketTags: TagsInput, ) { result: updateCloudNativeAwsStorageSetting(input: { - id: $id, - name: $name, - storageClass: $storageClass, - kmsMasterKeyId: $kmsMasterKeyId + id: $id, + name: $name, + storageClass: $storageClass, + kmsMasterKeyId: $kmsMasterKeyId, + deleteAllBucketTags: $deleteAllBucketTags + bucketTags: $bucketTags, }) { targetMapping { id diff --git a/pkg/polaris/graphql/aws/storage.go b/pkg/polaris/graphql/aws/storage.go deleted file mode 100644 index b894ecb7..00000000 --- a/pkg/polaris/graphql/aws/storage.go +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2023 Rubrik, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -package aws - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - "github.com/google/uuid" - "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" -) - -// Tag represents a key-value pair used to tag cloud resources. -type Tag struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// TargetMapping represents an AWS cloud archival location. -type TargetMapping struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - GroupType string `json:"groupType"` - TargetType string `json:"targetType"` - ConnectionStatus struct { - Status string `json:"status"` - } `json:"connectionStatus"` - TargetTemplate struct { - CloudAccount struct { - ID uuid.UUID `json:"id"` - } `json:"cloudAccount"` - BucketPrefix string `json:"bucketPrefix"` - StorageClass string `json:"storageClass"` - Region Region `json:"region"` - KMSMasterKey string `json:"kmsMasterKeyId"` - LocTemplate string `json:"cloudNativeLocTemplateType"` - BucketTags []Tag `json:"bucketTags"` - } -} - -// TargetMappingFilter is used to filter target mappings. -// -// Common field values are: -// - NAME - The name of the target mapping. Can also be used to search for a -// prefix of the name. -// - ARCHIVAL_GROUP_TYPE - The type of the archival group, e.g. -// CLOUD_NATIVE_ARCHIVAL_GROUP. -// - CLOUD_ACCOUNT_ID - The ID of an RSC cloud account. -// - ARCHIVAL_GROUP_ID - The ID of an archival group. Also known as target -// mapping ID. -type TargetMappingFilter struct { - Field string `json:"field"` - Text string `json:"text"` -} - -// AllTargetMappings returns all AWS target mappings that match the specified -// filter. In RSC cloud archival locations are also referred to as target -// mappings. -func (a API) AllTargetMappings(ctx context.Context, filter []TargetMappingFilter) ([]TargetMapping, error) { - a.log.Print(log.Trace) - - // Always filter for only AWS target mappings. - filter = append(filter, TargetMappingFilter{ - Field: "ARCHIVAL_LOCATION_TYPE", - Text: "AWS", - }) - buf, err := a.GQL.Request(ctx, allTargetMappingsQuery, struct { - Filter []TargetMappingFilter `json:"filter,omitempty"` - }{Filter: filter}) - if err != nil { - return nil, fmt.Errorf("failed to request allTargetMappings: %w", err) - } - a.log.Printf(log.Debug, "allTargetMappings(%v): %s", filter, string(buf)) - - var payload struct { - Data struct { - Result []TargetMapping `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return nil, fmt.Errorf("failed to unmarshal allTargetMappings: %v", err) - } - - return payload.Data.Result, nil -} - -// DeleteTargetMapping deletes the target mapping with the specified ID. In RSC -// cloud archival locations are also referred to as target mappings. -func (a API) DeleteTargetMapping(ctx context.Context, id uuid.UUID) error { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, deleteTargetMappingQuery, struct { - ID uuid.UUID `json:"id"` - }{ID: id}) - if err != nil { - return fmt.Errorf("failed to request deleteTargetMapping: %w", err) - } - a.log.Printf(log.Debug, "deleteTargetMapping(%q): %s", id, string(buf)) - - var payload struct { - Data struct { - Result string `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal deleteTargetMapping: %v", err) - } - - return nil -} - -// CreateCloudNativeStorageSetting creates a cloud native archival location. -// The KMS master key can be either a key alias or a key ID. Region and bucket -// tags are optional. -// -// Common storage class values are: -// - STANDARD -// - STANDARD_IA -// - ONEZONE_IA -// - GLACIER_INSTANT_RETRIEVAL -// -// Common location template type values are: -// - SOURCE_REGION -// - SPECIFIC_REGION -func (a API) CreateCloudNativeStorageSetting(ctx context.Context, id uuid.UUID, name, bucketPrefix, storageClass string, region Region, kmsMasterKey, locTemplateType string, bucketTags []Tag) (uuid.UUID, error) { - a.log.Print(log.Trace) - - tags := &struct { - TagList []Tag `json:"tagList"` - }{TagList: bucketTags} - if len(bucketTags) == 0 { - tags = nil - } - - buf, err := a.GQL.Request(ctx, createCloudNativeAwsStorageSettingQuery, struct { - CloudAccountID uuid.UUID `json:"cloudAccountId"` - Name string `json:"name"` - BucketPrefix string `json:"bucketPrefix"` - StorageClass string `json:"storageClass"` - Region Region `json:"region,omitempty"` - KmsMasterKey string `json:"kmsMasterKeyId"` - LocTemplate string `json:"locTemplateType"` - BucketTags *struct { - TagList []Tag `json:"tagList"` - } `json:"bucketTags,omitempty"` - }{CloudAccountID: id, Name: name, BucketPrefix: bucketPrefix, StorageClass: storageClass, Region: region, KmsMasterKey: kmsMasterKey, LocTemplate: locTemplateType, BucketTags: tags}) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to request createCloudNativeAwsStorageSetting: %w", err) - } - a.log.Printf(log.Debug, "createCloudNativeAwsStorageSetting(%q, %q, %q, %q, %q, , %q, %v): %s", - id, name, bucketPrefix, storageClass, region, locTemplateType, bucketTags, string(buf)) - - var payload struct { - Data struct { - Result struct { - TargetMapping struct { - ID uuid.UUID `json:"id"` - } `json:"targetMapping"` - } `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return uuid.Nil, fmt.Errorf("failed to unmarshal createCloudNativeAwsStorageSetting: %v", err) - } - - return payload.Data.Result.TargetMapping.ID, nil -} - -// UpdateCloudNativeStorageSetting updates the cloud native archival location -// with the specified ID. The KMS master key can be either a key alias or a key -// ID. Note that not all properties can be updated, only the name, storage and -// KMS master key. -func (a API) UpdateCloudNativeStorageSetting(ctx context.Context, id uuid.UUID, name string, storageClass string, kmsMasterKey string) error { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, updateCloudNativeAwsStorageSettingQuery, struct { - ID uuid.UUID `json:"id"` - Name string `json:"name,omitempty"` - StorageClass string `json:"storageClass,omitempty"` - KmsMasterKey string `json:"kmsMasterKeyId,omitempty"` - }{ID: id, Name: name, StorageClass: storageClass, KmsMasterKey: kmsMasterKey}) - if err != nil { - return fmt.Errorf("failed to request updateCloudNativeAwsStorageSetting: %w", err) - } - a.log.Printf(log.Debug, "updateCloudNativeAwsStorageSetting(%q, %q, %q ): %s", - id, name, storageClass, string(buf)) - - var payload struct { - Data struct { - Result struct { - TargetMapping struct { - ID uuid.UUID `json:"id"` - } `json:"targetMapping"` - } `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal createCloudNativeAwsStorageSetting: %v", err) - } - if id != payload.Data.Result.TargetMapping.ID { - return errors.New("wrong id returned from updateCloudNativeAwsStorageSetting") - } - - return nil -} diff --git a/pkg/polaris/graphql/aws/testdata/validate_and_create_aws_cloud_account.json b/pkg/polaris/graphql/aws/testdata/validate_and_create_aws_cloud_account_response.json similarity index 100% rename from pkg/polaris/graphql/aws/testdata/validate_and_create_aws_cloud_account.json rename to pkg/polaris/graphql/aws/testdata/validate_and_create_aws_cloud_account_response.json diff --git a/pkg/polaris/graphql/azure/archival.go b/pkg/polaris/graphql/azure/archival.go new file mode 100644 index 00000000..0008eeb0 --- /dev/null +++ b/pkg/polaris/graphql/azure/archival.go @@ -0,0 +1,149 @@ +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package azure + +import "github.com/google/uuid" + +// TargetMappingFilter is used to filter Azure target mappings. Common field +// values are: +// +// - NAME - The name of the target mapping. It can also be used to search for +// a prefix of the name. +// +// - ARCHIVAL_GROUP_TYPE - The type of the archival group, e.g., +// CLOUD_NATIVE_ARCHIVAL_GROUP. +// +// - CLOUD_ACCOUNT_ID - The ID of an RSC cloud account. +// +// - ARCHIVAL_GROUP_ID - The ID of an archival group. Also known as target +// mapping ID. +type TargetMappingFilter struct { + Field string `json:"field"` + Text string `json:"text,omitempty"` + TestList []string `json:"testList,omitempty"` +} + +// TargetMapping represents an Azure cloud archival location. +// Note, the ContainerNamePrefix field is not the prefix but the full container +// name. +type TargetMapping struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + GroupType string `json:"groupType"` + TargetType string `json:"targetType"` + ConnectionStatus struct { + Status string `json:"status"` + } `json:"connectionStatus"` + TargetTemplate struct { + CloudAccount struct { + ID uuid.UUID `json:"cloudAccountId"` + } `json:"cloudAccount"` + ContainerNamePrefix string `json:"containerNamePrefix"` + StorageAccountName string `json:"storageAccountName"` + CloudNativeCompanion struct { + LocTemplate string `json:"cloudNativeLocTemplateType"` + Redundancy string `json:"redundancy"` + StorageTier string `json:"storageTier"` + NativeID uuid.UUID `json:"subscriptionNativeId"` + StorageAccountRegion RegionEnum `json:"storageAccountRegion"` + StorageAccountTags []Tag `json:"storageAccountTags"` + CMKInfo []CustomerKey `json:"cmkInfo"` + } `json:"cloudNativeCompanion"` + } +} + +func (TargetMapping) ListQuery(filters []TargetMappingFilter) (string, any) { + return allTargetMappingsQuery, append(filters, TargetMappingFilter{ + Field: "ARCHIVAL_LOCATION_TYPE", + Text: "AZURE", + }) +} + +// StorageSettingCreateParams represents the parameters required to create an +// Azure storage setting. Note, the API ignores the ContainerName field and +// generates its own name. +type StorageSettingCreateParams struct { + LocTemplate string `json:"cloudNativeLocTemplateType"` + ContainerName string `json:"containerName"` + Name string `json:"name"` + Redundancy string `json:"redundancy"` + StorageTier string `json:"storageTier"` + NativeID uuid.UUID `json:"subscriptionNativeId"` + StorageAccountName string `json:"storageAccountName"` + StorageAccountRegion *RegionEnum `json:"storageAccountRegion,omitempty"` + StorageAccountTags *struct { + TagList []Tag `json:"tagList"` + } `json:"storageAccountTags,omitempty"` + CMKInfo []CustomerKey `json:"cmkInfo,omitempty"` +} + +// StorageSettingCreateResult represents the result of creating an Azure storage +// setting. +type StorageSettingCreateResult struct { + TargetMapping struct { + ID uuid.UUID `json:"id"` + } `json:"targetMapping"` +} + +func (StorageSettingCreateResult) CreateQuery(cloudAccountID uuid.UUID, createParams StorageSettingCreateParams) (string, any) { + return createCloudNativeAzureStorageSettingQuery, struct { + CloudAccountID uuid.UUID `json:"cloudAccountId"` + StorageSettingCreateParams + }{CloudAccountID: cloudAccountID, StorageSettingCreateParams: createParams} +} + +func (r StorageSettingCreateResult) Validate() (uuid.UUID, error) { + return r.TargetMapping.ID, nil +} + +// StorageSettingUpdateParams represents the parameters required to update an +// Azure storage setting. +type StorageSettingUpdateParams struct { + Name string `json:"name"` + StorageTier string `json:"storageTier"` + StorageAccountTags struct { + TagList []Tag `json:"tagList"` + } `json:"storageAccountTags"` + CMKInfo []CustomerKey `json:"cmkInfo,omitempty"` +} + +// StorageSettingUpdateResult represents the result of updating an Azure storage +// setting. +type StorageSettingUpdateResult StorageSettingCreateResult + +func (r StorageSettingUpdateResult) UpdateQuery(targetMappingID uuid.UUID, updateParams StorageSettingUpdateParams) (string, any) { + return updateCloudNativeAzureStorageSettingQuery, struct { + ID uuid.UUID `json:"id"` + StorageSettingUpdateParams + }{ID: targetMappingID, StorageSettingUpdateParams: updateParams} +} + +func (r StorageSettingUpdateResult) Validate() (uuid.UUID, error) { + return r.TargetMapping.ID, nil +} + +// CustomerKey represents the customer managed key information required for +// encryption of Azure storage. +type CustomerKey struct { + KeyName string `json:"keyName"` + KeyVaultName string `json:"keyVaultName"` + Region RegionEnum `json:"region"` +} diff --git a/pkg/polaris/graphql/azure/azure.go b/pkg/polaris/graphql/azure/azure.go index 4aaebc83..6ce9932a 100644 --- a/pkg/polaris/graphql/azure/azure.go +++ b/pkg/polaris/graphql/azure/azure.go @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package azure provides a low level interface to the Azure GraphQL queries +// Package azure provides a low-level interface to the Azure GraphQL queries // provided by the Polaris platform. package azure @@ -28,8 +28,6 @@ import ( "context" "encoding/json" "errors" - "fmt" - "strings" "github.com/google/uuid" @@ -45,173 +43,6 @@ const ( PublicCloud Cloud = "AZUREPUBLICCLOUD" ) -// ProtectionFeature represents the protection features of an Azure cloud -// account. -type ProtectionFeature string - -const ( - // SQLDB Azure SQL Database. - SQLDB ProtectionFeature = "SQL_DB" - - // SQLMI Azure SQL Managed Instance. - SQLMI ProtectionFeature = "SQL_MI" - - // VM Azure Virtual Machine. - VM ProtectionFeature = "VM" -) - -// Region represents an Azure region in Polaris. -type Region string - -const ( - RegionUnknown Region = "UNKNOWN_AZURE_REGION" - RegionAustraliaCentral Region = "AUSTRALIACENTRAL" - RegionAustraliaCentral2 Region = "AUSTRALIACENTRAL2" - RegionAustraliaEast Region = "AUSTRALIAEAST" - RegionAustraliaSouthEast Region = "AUSTRALIASOUTHEAST" - RegionBrazilSouth Region = "BRAZILSOUTH" - RegionCanadaCentral Region = "CANADACENTRAL" - RegionCanadaEast Region = "CANADAEAST" - RegionCentralIndia Region = "CENTRALINDIA" - RegionCentralUS Region = "CENTRALUS" - RegionChinaEast Region = "CHINAEAST" - RegionChinaEast2 Region = "CHINAEAST2" - RegionChinaNorth Region = "CHINANORTH" - RegionChinaNorth2 Region = "CHINANORTH2" - RegionEastAsia Region = "EASTASIA" - RegionEastUS Region = "EASTUS" - RegionEastUS2 Region = "EASTUS2" - RegionFranceCentral Region = "FRANCECENTRAL" - RegionFranceSouth Region = "FRANCESOUTH" - RegionGermanyNorth Region = "GERMANYNORTH" - RegionGermanyWestCentral Region = "GERMANYWESTCENTRAL" - RegionJapanEast Region = "JAPANEAST" - RegionJapanWest Region = "JAPANWEST" - RegionKoreaCentral Region = "KOREACENTRAL" - RegionKoreaSouth Region = "KOREASOUTH" - RegionNorthCentralUS Region = "NORTHCENTRALUS" - RegionNorthEurope Region = "NORTHEUROPE" - RegionNorwayEast Region = "NORWAYEAST" - RegionNorwayWest Region = "NORWAYWEST" - RegionSouthAfricaNorth Region = "SOUTHAFRICANORTH" - RegionSouthAfricaWest Region = "SOUTHAFRICAWEST" - RegionSouthCentralUS Region = "SOUTHCENTRALUS" - RegionSouthEastAsia Region = "SOUTHEASTASIA" - RegionSouthIndia Region = "SOUTHINDIA" - RegionSwitzerlandNorth Region = "SWITZERLANDNORTH" - RegionSwitzerlandWest Region = "SWITZERLANDWEST" - RegionUAECentral Region = "UAECENTRAL" - RegionUAENorth Region = "UAENORTH" - RegionUKSouth Region = "UKSOUTH" - RegionUKWest Region = "UKWEST" - RegionWestCentralUS Region = "WESTCENTRALUS" - RegionWestEurope Region = "WESTEUROPE" - RegionWestIndia Region = "WESTINDIA" - RegionWestUS Region = "WESTUS" - RegionWestUS2 Region = "WESTUS2" - RegionWestUS3 Region = "WESTUS3" -) - -// FormatRegion returns the Region as a string formatted in Azure's style, i.e. -// lower case. -func FormatRegion(region Region) string { - return strings.ToLower(string(region)) -} - -// FormatRegions returns the Regions as a slice of strings formatted in Azure's -// style, i.e. lower case. -func FormatRegions(regions []Region) []string { - regs := make([]string, 0, len(regions)) - for _, region := range regions { - regs = append(regs, FormatRegion(region)) - } - - return regs -} - -var validRegions = map[Region]struct{}{ - RegionAustraliaCentral: {}, - RegionAustraliaCentral2: {}, - RegionAustraliaEast: {}, - RegionAustraliaSouthEast: {}, - RegionBrazilSouth: {}, - RegionCanadaCentral: {}, - RegionCanadaEast: {}, - RegionCentralIndia: {}, - RegionCentralUS: {}, - RegionChinaEast: {}, - RegionChinaEast2: {}, - RegionChinaNorth: {}, - RegionChinaNorth2: {}, - RegionEastAsia: {}, - RegionEastUS: {}, - RegionEastUS2: {}, - RegionFranceCentral: {}, - RegionFranceSouth: {}, - RegionGermanyNorth: {}, - RegionGermanyWestCentral: {}, - RegionJapanEast: {}, - RegionJapanWest: {}, - RegionKoreaCentral: {}, - RegionKoreaSouth: {}, - RegionNorthCentralUS: {}, - RegionNorthEurope: {}, - RegionNorwayEast: {}, - RegionNorwayWest: {}, - RegionSouthAfricaNorth: {}, - RegionSouthAfricaWest: {}, - RegionSouthCentralUS: {}, - RegionSouthEastAsia: {}, - RegionSouthIndia: {}, - RegionSwitzerlandNorth: {}, - RegionSwitzerlandWest: {}, - RegionUAECentral: {}, - RegionUAENorth: {}, - RegionUKSouth: {}, - RegionUKWest: {}, - RegionWestCentralUS: {}, - RegionWestEurope: {}, - RegionWestIndia: {}, - RegionWestUS: {}, - RegionWestUS2: {}, - RegionWestUS3: {}, -} - -// ParseRegion returns the Region matching the given region. Accepts both -// Polaris and Azure style region names. -func ParseRegion(region string) (Region, error) { - // Polaris region name. - r := Region(region) - if _, ok := validRegions[r]; ok { - return r, nil - } - - // Azure region name. - r = Region(strings.ToUpper(region)) - if _, ok := validRegions[r]; ok { - return r, nil - } - - return RegionUnknown, errors.New("invalid azure region") -} - -// ParseRegions returns the Regions matching the given regions. Accepts both -// Polaris and Azure style region names. -func ParseRegions(regions []string) ([]Region, error) { - regs := make([]Region, 0, len(regions)) - - for _, r := range regions { - region, err := ParseRegion(r) - if err != nil { - return nil, fmt.Errorf("failed to parse region: %v", err) - } - - regs = append(regs, region) - } - - return regs, nil -} - // API wraps around GraphQL clients to give them the RSC Azure API. type API struct { Version string // Deprecated: use GQL.DeploymentVersion @@ -231,7 +62,8 @@ func Wrap(gql *graphql.Client) API { func (a API) SetCloudAccountCustomerAppCredentials(ctx context.Context, cloud Cloud, appID, appTenantID uuid.UUID, appName, appTenantDomain, appSecretKey string, shouldReplace bool) error { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, setAzureCloudAccountCustomerAppCredentialsQuery, struct { + query := setAzureCloudAccountCustomerAppCredentialsQuery + buf, err := a.GQL.RequestWithoutLogging(ctx, query, struct { Cloud Cloud `json:"azureCloudType"` ID uuid.UUID `json:"appId"` Name string `json:"appName"` @@ -241,10 +73,10 @@ func (a API) SetCloudAccountCustomerAppCredentials(ctx context.Context, cloud Cl ShouldReplace bool `json:"shouldReplace"` }{Cloud: cloud, ID: appID, Name: appName, TenantID: appTenantID, TenantDomain: appTenantDomain, SecretKey: appSecretKey, ShouldReplace: shouldReplace}) if err != nil { - return fmt.Errorf("failed to request setAzureCloudAccountCustomerAppCredentialsQuery: %w", err) + return graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "setAzureCloudAccountCustomerAppCredentialsQuery(%v, %v, %v, \"\", %v, %v, %v): %s", cloud, - appID, appName, appTenantID, appTenantDomain, shouldReplace, string(buf)) + a.log.Printf(log.Debug, "%s(%q, %q, %q, , %q, %q, %t): %s", graphql.QueryName(query), cloud, appID, + appName, appTenantID, appTenantDomain, shouldReplace, string(buf)) var payload struct { Data struct { @@ -252,10 +84,10 @@ func (a API) SetCloudAccountCustomerAppCredentials(ctx context.Context, cloud Cl } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal setAzureCloudAccountCustomerAppCredentialsQuery: %v", err) + return graphql.UnmarshalError(query, err) } if !payload.Data.Result { - return errors.New("set app credentials failed") + return graphql.ResponseError(query, errors.New("set app credentials failed")) } return nil diff --git a/pkg/polaris/graphql/azure/cloud.go b/pkg/polaris/graphql/azure/cloud.go index 9a869c4b..ddfcf2db 100644 --- a/pkg/polaris/graphql/azure/cloud.go +++ b/pkg/polaris/graphql/azure/cloud.go @@ -24,15 +24,26 @@ import ( "context" "encoding/json" "errors" - "fmt" "github.com/google/uuid" - + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -// CloudAccount represents an RSC Cloud Account for Azure. +// CloudAccountTenant hold details about an Azure tenant and the cloud accounts +// associated with the tenant. +type CloudAccountTenant struct { + Cloud Cloud `json:"cloudType"` + ID uuid.UUID `json:"azureCloudAccountTenantRubrikId"` + ClientID uuid.UUID `json:"clientId"` + AppName string `json:"appName"` + DomainName string `json:"domainName"` + SubscriptionCount int `json:"subscriptionCount"` + Accounts []CloudAccount `json:"subscriptions"` +} + +// CloudAccount represents an RSC Azure cloud account. type CloudAccount struct { ID uuid.UUID `json:"id"` NativeID uuid.UUID `json:"nativeId"` @@ -40,25 +51,33 @@ type CloudAccount struct { Feature Feature `json:"featureDetail"` } -// Feature represents an RSC Cloud Account feature for Azure, e.g. Cloud Native +// Feature represents an RSC Cloud Account feature for Azure, e.g., Cloud Native // Protection. type Feature struct { - Feature string `json:"feature"` - Regions []Region `json:"regions"` - Status core.Status `json:"status"` + Feature string `json:"feature"` + ResourceGroup FeatureResourceGroup `json:"resourceGroup"` + Regions []CloudAccountRegionEnum `json:"regions"` + Status core.Status `json:"status"` + UserAssignedManagedIdentity FeatureUserAssignedManagedIdentity `json:"userAssignedManagedIdentity"` } -// CloudAccountTenant hold details about an Azure tenant and the cloud -// account associated with that tenant. -type CloudAccountTenant struct { - Cloud Cloud `json:"cloudType"` - ID uuid.UUID `json:"azureCloudAccountTenantRubrikId"` - ClientID uuid.UUID `json:"clientId"` - DomainName string `json:"domainName"` - Accounts []CloudAccount `json:"subscriptions"` +// FeatureResourceGroup represents a resource group for a particular feature. +type FeatureResourceGroup struct { + Name string `json:"name"` + NativeID string `json:"nativeId"` + Tags []Tag `json:"tags"` + Region NativeRegionEnum `json:"region"` +} + +// ResourceGroup holds the information for a resource group when a particular +// feature is onboarded. +type ResourceGroup struct { + Name string `json:"name"` + TagList TagList `json:"tags"` + Region CloudAccountRegionEnum `json:"region"` } -// Tag represents tags to be applied to Azure resource. +// Tag represents the tags present in the resource group. type Tag struct { Key string `json:"key"` Value string `json:"value"` @@ -69,82 +88,64 @@ type TagList struct { Tags []Tag `json:"tagList"` } -// ResourceGroup contains the information of resource group -// created for a particular feature. -type ResourceGroup struct { - Name string `json:"name"` - TagList *TagList `json:"tags"` - Region `json:"region"` +// FeatureUserAssignedManagedIdentity represents a user-assigned managed +// identity for a particular feature. +type FeatureUserAssignedManagedIdentity struct { + Name string `json:"name"` + NativeId string `json:"nativeId"` + PrincipalID string `json:"principalId"` } -// FeatureSpecificInfo represents feature specific information. -// Supports: -// 1. Managed Identity for Archival Encryption feature. -type FeatureSpecificInfo struct { - UserAssignedManagedIdentity *UserAssignedManagedIdentity `json:"userAssignedManagedIdentityInput"` -} - -// UserAssignedManagedIdentity represents the managed identity -// information for archival. +// UserAssignedManagedIdentity holds the information for a user-assigned managed +// identity when a particular feature is onboarded. type UserAssignedManagedIdentity struct { - Name string `json:"name"` - ResourceGroupName string `json:"resourceGroupName"` - PrincipalID string `json:"principalId"` - Region `json:"region"` + Name string `json:"name"` + ResourceGroupName string `json:"resourceGroupName"` + PrincipalID string `json:"principalId"` + Region CloudAccountRegionEnum `json:"region"` } -// CloudAccountFeature represents feature information for -// specific cloud native azure features. +// CloudAccountFeature holds the information for a particular feature when it's +// onboarded. type CloudAccountFeature struct { - PolicyVersion int `json:"policyVersion"` - ResourceGroup *ResourceGroup `json:"resourceGroup"` - FeatureType string `json:"featureType"` - FeatureSpecificInfo *FeatureSpecificInfo `json:"specificFeatureInput"` + PolicyVersion int `json:"policyVersion"` + PermissionGroups []PermissionGroupWithVersion `json:"permissionsGroups,omitempty"` + ResourceGroup *ResourceGroup `json:"resourceGroup,omitempty"` + FeatureType string `json:"featureType"` + FeatureSpecificInfo *FeatureSpecificInfo `json:"specificFeatureInput,omitempty"` } -// CloudAccountTenant returns the tenant and cloud accounts for the specified -// feature and Polaris tenant id. The filter can be used to search for -// subscription name and subscription id. -func (a API) CloudAccountTenant(ctx context.Context, id uuid.UUID, feature core.Feature, filter string) (CloudAccountTenant, error) { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, azureCloudAccountTenantQuery, struct { - ID uuid.UUID `json:"tenantId"` - Feature string `json:"feature"` - Filter string `json:"subscriptionSearchText"` - }{ID: id, Feature: feature.Name, Filter: filter}) - if err != nil { - return CloudAccountTenant{}, fmt.Errorf("failed to request azureCloudAccountTenant: %w", err) - } - a.log.Printf(log.Debug, "azureCloudAccountTenantQuery(%q, %q, %q): %s", id, feature, filter, - string(buf)) - - var payload struct { - Data struct { - Result CloudAccountTenant `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return CloudAccountTenant{}, fmt.Errorf("failed to unmarshal azureCloudAccountTenantQuery: %v", err) - } +// PermissionGroupWithVersion represents a permission group, and its version +// for a particular feature. +type PermissionGroupWithVersion struct { + PermissionGroup string `json:"permissionsGroup"` + Version int `json:"version"` +} - return payload.Data.Result, nil +// FeatureSpecificInfo represents feature specific information. +// Supports: +// +// 1. User-assigned managed identity for the Cloud Native Archival Encryption +// feature. +type FeatureSpecificInfo struct { + UserAssignedManagedIdentity *UserAssignedManagedIdentity `json:"userAssignedManagedIdentityInput,omitempty"` } // CloudAccountTenants return all tenants for the specified feature. If -// includeSubscription is true all cloud accounts for each tenant are also -// returned. Note that this function does not support AllFeatures. +// includeSubscription is true, all cloud accounts for each tenant are also +// returned. func (a API) CloudAccountTenants(ctx context.Context, feature core.Feature, includeSubscriptions bool) ([]CloudAccountTenant, error) { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, allAzureCloudAccountTenantsQuery, struct { + query := allAzureCloudAccountTenantsQuery + buf, err := a.GQL.Request(ctx, query, struct { Feature string `json:"feature"` IncludeSubscriptions bool `json:"includeSubscriptionDetails"` }{Feature: feature.Name, IncludeSubscriptions: includeSubscriptions}) if err != nil { - return nil, fmt.Errorf("failed to request allAzureCloudAccountTenants: %w", err) + return nil, graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "allAzureCloudAccountTenants(%q, %t): %s", feature.Name, includeSubscriptions, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -152,7 +153,7 @@ func (a API) CloudAccountTenants(ctx context.Context, feature core.Feature, incl } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return nil, fmt.Errorf("failed to unmarshal allAzureCloudAccountTenants: %v", err) + return nil, graphql.UnmarshalError(query, err) } return payload.Data.Result, nil @@ -162,22 +163,28 @@ func (a API) CloudAccountTenants(ctx context.Context, feature core.Feature, incl // for given feature without OAuth. func (a API) AddCloudAccountWithoutOAuth(ctx context.Context, cloud Cloud, id uuid.UUID, feature CloudAccountFeature, name, tenantDomain string, regions []Region) (string, error) { - a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, addAzureCloudAccountWithoutOauthQuery, struct { - Cloud Cloud `json:"azureCloudType"` - Feature CloudAccountFeature `json:"feature"` - SubscriptionName string `json:"subscriptionName"` - SubscriptionID uuid.UUID `json:"subscriptionId"` - TenantDomain string `json:"tenantDomainName"` - Regions []Region `json:"regions"` - }{Cloud: cloud, Feature: feature, SubscriptionName: name, SubscriptionID: id, TenantDomain: tenantDomain, Regions: regions}) + query := addAzureCloudAccountWithoutOauthQuery + buf, err := a.GQL.Request(ctx, query, struct { + Cloud Cloud `json:"azureCloudType"` + Feature CloudAccountFeature `json:"feature"` + SubscriptionName string `json:"subscriptionName"` + SubscriptionID uuid.UUID `json:"subscriptionId"` + TenantDomain string `json:"tenantDomainName"` + Regions []CloudAccountRegionEnum `json:"regions"` + }{ + Cloud: cloud, + Feature: feature, + SubscriptionName: name, + SubscriptionID: id, + TenantDomain: tenantDomain, + Regions: RegionsToCloudAccountRegionEnum(regions), + }) if err != nil { - return "", fmt.Errorf("failed to request addAzureCloudAccountWithoutOauth: %w", err) + return "", graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "addAzureCloudAccountWithoutOauth(%q, %q, %q, %q, %q, %q, %d): %s", cloud, id, - feature.FeatureType, name, tenantDomain, regions, feature.PolicyVersion, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -192,13 +199,13 @@ func (a API) AddCloudAccountWithoutOAuth(ctx context.Context, cloud Cloud, id uu } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return "", fmt.Errorf("failed to unmarshal addAzureCloudAccountWithoutOauth: %v", err) + return "", graphql.UnmarshalError(query, err) } if len(payload.Data.Result.Status) != 1 { - return "", errors.New("expected a single result") + return "", graphql.ResponseError(query, errors.New("expected a single result")) } if payload.Data.Result.Status[0].Error != "" { - return "", errors.New(payload.Data.Result.Status[0].Error) + return "", graphql.ResponseError(query, errors.New(payload.Data.Result.Status[0].Error)) } return payload.Data.Result.TenantID, nil @@ -209,14 +216,15 @@ func (a API) AddCloudAccountWithoutOAuth(ctx context.Context, cloud Cloud, id uu func (a API) DeleteCloudAccountWithoutOAuth(ctx context.Context, id uuid.UUID, feature core.Feature) error { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, deleteAzureCloudAccountWithoutOauthQuery, struct { + query := deleteAzureCloudAccountWithoutOauthQuery + buf, err := a.GQL.Request(ctx, query, struct { IDs []uuid.UUID `json:"subscriptionIds"` Features []string `json:"features"` }{IDs: []uuid.UUID{id}, Features: []string{feature.Name}}) if err != nil { - return fmt.Errorf("failed to request deleteAzureCloudAccountWithoutOauth: %w", err) + return graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "deleteAzureCloudAccountWithoutOauth(%v, %q): %s", id, feature.Name, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -230,39 +238,43 @@ func (a API) DeleteCloudAccountWithoutOAuth(ctx context.Context, id uuid.UUID, f } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal deleteAzureCloudAccountWithoutOauth: %v", err) + return graphql.UnmarshalError(query, err) } if len(payload.Data.Result.Status) != 1 { - return errors.New("expected a single result") + return graphql.ResponseError(query, errors.New("expected a single result")) } if !payload.Data.Result.Status[0].Success { - return errors.New(payload.Data.Result.Status[0].Error) + return graphql.ResponseError(query, errors.New(payload.Data.Result.Status[0].Error)) } return nil } -type updateSubscription struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` -} - // UpdateCloudAccount updates the name and the regions for the cloud account // with the specified RSC cloud account id. func (a API) UpdateCloudAccount(ctx context.Context, id uuid.UUID, feature core.Feature, name string, toAdd, toRemove []Region) error { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, updateAzureCloudAccountQuery, struct { - Features []string `json:"features"` - ToAdd []Region `json:"regionsToAdd,omitempty"` - ToRemove []Region `json:"regionsToRemove,omitempty"` - Subscriptions []updateSubscription `json:"subscriptions"` - }{Features: []string{feature.Name}, ToAdd: toAdd, ToRemove: toRemove, Subscriptions: []updateSubscription{{ID: id, Name: name}}}) + type updateSubscription struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + } + query := updateAzureCloudAccountQuery + buf, err := a.GQL.Request(ctx, query, struct { + Features []string `json:"features"` + ToAdd []CloudAccountRegionEnum `json:"regionsToAdd,omitempty"` + ToRemove []CloudAccountRegionEnum `json:"regionsToRemove,omitempty"` + Subscriptions []updateSubscription `json:"subscriptions"` + }{ + Features: []string{feature.Name}, + ToAdd: RegionsToCloudAccountRegionEnum(toAdd), + ToRemove: RegionsToCloudAccountRegionEnum(toRemove), + Subscriptions: []updateSubscription{{ID: id, Name: name}}, + }) if err != nil { - return fmt.Errorf("failed to request updateAzureCloudAccount: %w", err) + return graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "updateAzureCloudAccount(%q, %v, %v %v, %v): %s", id, feature.Name, name, toAdd, - toRemove, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -275,13 +287,13 @@ func (a API) UpdateCloudAccount(ctx context.Context, id uuid.UUID, feature core. } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal updateAzureCloudAccount: %v", err) + return graphql.UnmarshalError(query, err) } if len(payload.Data.Result.Status) != 1 { - return errors.New("expected a single result") + return graphql.ResponseError(query, errors.New("expected a single result")) } if !payload.Data.Result.Status[0].Success { - return errors.New("update cloud account failed") + return graphql.ResponseError(query, errors.New("update cloud account failed")) } return nil @@ -292,8 +304,15 @@ func (a API) UpdateCloudAccount(ctx context.Context, id uuid.UUID, feature core. // the Azure role for the subscription. ExcludedActions refers to actions which // should be explicitly disallowed on the Azure role for the subscription. type PermissionConfig struct { - PermissionVersion int `json:"permissionVersion"` - RolePermissions []struct { + PermissionVersion int `json:"permissionVersion"` + PermissionGroupVersions []PermissionGroupWithVersion `json:"permissionsGroupVersions"` + ResourceGroupRolePermissions []struct { + ExcludedActions []string `json:"excludedActions"` + ExcludedDataActions []string `json:"excludedDataActions"` + IncludedActions []string `json:"includedActions"` + IncludedDataActions []string `json:"includedDataActions"` + } `json:"resourceGroupRolePermissions"` + RolePermissions []struct { ExcludedActions []string `json:"excludedActions"` ExcludedDataActions []string `json:"excludedDataActions"` IncludedActions []string `json:"includedActions"` @@ -306,41 +325,42 @@ type PermissionConfig struct { func (a API) CloudAccountPermissionConfig(ctx context.Context, feature core.Feature) (PermissionConfig, error) { a.log.Print(log.Trace) + query := azureCloudAccountPermissionConfigQuery buf, err := a.GQL.Request(ctx, azureCloudAccountPermissionConfigQuery, struct { Feature string `json:"feature"` }{Feature: feature.Name}) if err != nil { - return PermissionConfig{}, fmt.Errorf("failed to request azureCloudAccountPermissionConfig: %w", err) + return PermissionConfig{}, graphql.RequestError(query, err) } - - a.log.Printf(log.Debug, "azureCloudAccountPermissionConfig(%q): %s", feature, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { - PermissionConfig PermissionConfig `json:"azureCloudAccountPermissionConfig"` + Result PermissionConfig `json:"result"` } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return PermissionConfig{}, fmt.Errorf("failed to unmarshal azureCloudAccountPermissionConfig: %v", err) + return PermissionConfig{}, graphql.UnmarshalError(query, err) } - return payload.Data.PermissionConfig, nil + return payload.Data.Result, nil } // UpgradeCloudAccountPermissionsWithoutOAuth notifies RSC that the permissions -// for the Azure service principal has been updated for the specified RSC cloud +// for the Azure service principal have been updated for the specified RSC cloud // account id and feature. func (a API) UpgradeCloudAccountPermissionsWithoutOAuth(ctx context.Context, id uuid.UUID, feature core.Feature) error { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, upgradeAzureCloudAccountPermissionsWithoutOauthQuery, struct { + query := upgradeAzureCloudAccountPermissionsWithoutOauthQuery + buf, err := a.GQL.Request(ctx, query, struct { ID uuid.UUID `json:"cloudAccountId"` Feature string `json:"feature"` }{ID: id, Feature: feature.Name}) if err != nil { - return fmt.Errorf("failed to request upgradeAzureCloudAccountPermissionsWithoutOauth: %w", err) + return graphql.RequestError(query, err) } - a.log.Printf(log.Debug, "upgradeAzureCloudAccountPermissionsWithoutOauth(%q, %q): %s", id, feature.Name, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -350,10 +370,10 @@ func (a API) UpgradeCloudAccountPermissionsWithoutOAuth(ctx context.Context, id } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal upgradeAzureCloudAccountPermissionsWithoutOauth: %v", err) + return graphql.UnmarshalError(query, err) } if !payload.Data.Result.Status { - return errors.New("update cloud account permissions failed") + return graphql.ResponseError(query, errors.New("update cloud account permissions failed")) } return nil @@ -365,14 +385,15 @@ func (a API) UpgradeCloudAccountPermissionsWithoutOAuth(ctx context.Context, id func (a API) StartDisableCloudAccountJob(ctx context.Context, id uuid.UUID, feature core.Feature) (uuid.UUID, error) { a.GQL.Log().Print(log.Trace) + query := startDisableAzureCloudAccountJobQuery buf, err := a.GQL.Request(ctx, startDisableAzureCloudAccountJobQuery, struct { ID uuid.UUID `json:"cloudAccountId"` Feature string `json:"feature"` }{ID: id, Feature: feature.Name}) if err != nil { - return uuid.Nil, fmt.Errorf("failed to request StartDisableCloudAccountJob: %w", err) + return uuid.Nil, graphql.RequestError(query, err) } - a.GQL.Log().Printf(log.Debug, "startDisableAzureCloudAccountJobQuery(%q, %q): %s", id, feature.Name, string(buf)) + graphql.LogResponse(a.log, query, buf) var payload struct { Data struct { @@ -387,14 +408,25 @@ func (a API) StartDisableCloudAccountJob(ctx context.Context, id uuid.UUID, feat } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return uuid.Nil, fmt.Errorf("failed to unmarshal StartDisableCloudAccountJob: %v", err) + return uuid.Nil, graphql.UnmarshalError(query, err) } if len(payload.Data.Result.Errors) != 0 { - return uuid.Nil, errors.New(payload.Data.Result.Errors[0].Error) + return uuid.Nil, graphql.ResponseError(query, errors.New(payload.Data.Result.Errors[0].Error)) } if len(payload.Data.Result.JobIDs) != 1 { - return uuid.Nil, fmt.Errorf("expected a single result") + return uuid.Nil, graphql.ResponseError(query, errors.New("expected a single result")) } return payload.Data.Result.JobIDs[0].JobID, nil } + +// RegionsToCloudAccountRegionEnum converts a slice of Regions to a slice of +// CloudAccountRegionEnums. +func RegionsToCloudAccountRegionEnum(regions []Region) []CloudAccountRegionEnum { + enums := make([]CloudAccountRegionEnum, 0, len(regions)) + for _, region := range regions { + enums = append(enums, region.ToCloudAccountRegionEnum()) + } + + return enums +} diff --git a/pkg/polaris/graphql/azure/exocompute.go b/pkg/polaris/graphql/azure/exocompute.go index 8de46449..d8d696a5 100644 --- a/pkg/polaris/graphql/azure/exocompute.go +++ b/pkg/polaris/graphql/azure/exocompute.go @@ -21,129 +21,144 @@ package azure import ( - "context" - "encoding/json" "errors" - "fmt" "github.com/google/uuid" - - "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -// ExocomputeConfig represents a single exocompute config. -type ExocomputeConfig struct { - ID uuid.UUID `json:"configUuid"` - Region Region `json:"region"` - SubnetID string `json:"subnetNativeId"` - Message string `json:"message"` - - // When true Rubrik will manage the security groups. - IsManagedByRubrik bool `json:"isRscManaged"` -} - -// ExocomputeConfigsForAccount holds all exocompute configs for a specific +// ExoConfigsForAccount holds all exocompute configurations for a specific // account. -type ExocomputeConfigsForAccount struct { - Account CloudAccount `json:"azureCloudAccount"` - Configs []ExocomputeConfig `json:"configs"` - EligibleRegions []string `json:"exocomputeEligibleRegions"` - Feature Feature `json:"featureDetails"` +type ExoConfigsForAccount struct { + Account CloudAccount `json:"azureCloudAccount"` + Configs []ExoConfig `json:"configs"` + EligibleRegions []CloudAccountRegionEnum `json:"exocomputeEligibleRegions"` + Feature Feature `json:"featureDetails"` } -// ExocomputeConfigs returns all exocompute configs matching the specified -// filter. The filter can be used to search for account name or account id. -func (a API) ExocomputeConfigs(ctx context.Context, filter string) ([]ExocomputeConfigsForAccount, error) { - a.log.Print(log.Trace) - - buf, err := a.GQL.Request(ctx, allAzureExocomputeConfigsInAccountQuery, struct { +func (r ExoConfigsForAccount) ListQuery(filter string) (string, any) { + return allAzureExocomputeConfigsInAccountQuery, struct { Filter string `json:"azureExocomputeSearchQuery"` - }{Filter: filter}) - if err != nil { - return nil, fmt.Errorf("failed to request allAzureExocomputeConfigsInAccount: %w", err) - } - a.log.Printf(log.Debug, "allAzureExocomputeConfigsInAccount(%q): %s", filter, string(buf)) - - var payload struct { - Data struct { - Result []ExocomputeConfigsForAccount `json:"result"` - } `json:"data"` - } - if err := json.Unmarshal(buf, &payload); err != nil { - return nil, fmt.Errorf("failed to unmarshal allAzureExocomputeConfigsInAccount: %v", err) - } + }{Filter: filter} +} - return payload.Data.Result, nil +// ExoConfig represents a single exocompute configuration. +type ExoConfig struct { + ID string `json:"configUuid"` + Region CloudAccountRegionEnum `json:"region"` + SubnetID string `json:"subnetNativeId"` + Message string `json:"message"` + ManagedByRubrik bool `json:"isRscManaged"` // When true, Rubrik will manage the security groups. + PodOverlayNetworkCIDR string `json:"podOverlayNetworkCidr"` + PodSubnetID string `json:"podSubnetNativeId"` + + // HealthCheckStatus represents the health status of an exocompute cluster. + HealthCheckStatus struct { + Status string `json:"status"` + FailureReason string `json:"failureReason"` + LastUpdatedAt string `json:"lastUpdatedAt"` + TaskchainID string `json:"taskchainId"` + } `json:"healthCheckStatus"` } -// ExocomputeConfigCreate represents an exocompute config to be created by RSC. -type ExocomputeConfigCreate struct { - Region Region `json:"region"` - SubnetID string `json:"subnetNativeId"` +// ExoCreateParams represents the parameters required to create an Azure +// exocompute configuration. +type ExoCreateParams struct { + Region CloudAccountRegionEnum `json:"region"` + SubnetID string `json:"subnetNativeId"` + IsManagedByRubrik bool `json:"isRscManaged"` // When true, Rubrik will manage the security groups. + PodOverlayNetworkCIDR string `json:"podOverlayNetworkCidr,omitempty"` + PodSubnetID string `json:"podSubnetNativeId,omitempty"` +} - // When true Rubrik will manage the security groups. - IsManagedByRubrik bool `json:"isRscManaged"` +// ExoCreateResult represents the result of creating an Azure exocompute +// configuration. +type ExoCreateResult struct { + Configs []ExoConfig `json:"configs"` } -// AddCloudAccountExocomputeConfigurations creates a new exocompute config for -// the account with the specified RSC cloud account id. Returns the created -// exocompute config -func (a API) AddCloudAccountExocomputeConfigurations(ctx context.Context, id uuid.UUID, config ExocomputeConfigCreate) (ExocomputeConfig, error) { - a.log.Print(log.Trace) +func (ExoCreateResult) CreateQuery(cloudAccountID uuid.UUID, createParams ExoCreateParams) (string, any) { + return addAzureCloudAccountExocomputeConfigurationsQuery, struct { + ID uuid.UUID `json:"cloudAccountId"` + Configs []ExoCreateParams `json:"azureExocomputeRegionConfigs"` + }{ID: cloudAccountID, Configs: []ExoCreateParams{createParams}} +} - buf, err := a.GQL.Request(ctx, addAzureCloudAccountExocomputeConfigurationsQuery, struct { - ID uuid.UUID `json:"cloudAccountId"` - Configs []ExocomputeConfigCreate `json:"azureExocomputeRegionConfigs"` - }{ID: id, Configs: []ExocomputeConfigCreate{config}}) - if err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to request addAzureCloudAccountExocomputeConfigurations: %w", err) +func (r ExoCreateResult) Validate() (uuid.UUID, error) { + if len(r.Configs) != 1 { + return uuid.Nil, errors.New("expected a single create result") } - a.log.Printf(log.Debug, "addAzureCloudAccountExocomputeConfigurations(%q, %v): %s", id, config, string(buf)) - - var payload struct { - Data struct { - Result struct { - Configs []ExocomputeConfig `json:"configs"` - } `json:"result"` - } `json:"data"` + if msg := r.Configs[0].Message; msg != "" { + return uuid.Nil, errors.New(msg) } - if err := json.Unmarshal(buf, &payload); err != nil { - return ExocomputeConfig{}, fmt.Errorf("failed to unmarshal addAzureCloudAccountExocomputeConfigurations: %v", err) - } - if len(payload.Data.Result.Configs) != 1 { - return ExocomputeConfig{}, errors.New("expected a single result") + id, err := uuid.Parse(r.Configs[0].ID) + if err != nil { + return uuid.Nil, err } - return payload.Data.Result.Configs[0], nil + return id, nil } -// DeleteCloudAccountExocomputeConfigurations deletes the exocompute config -// with the specified RSC exocompute config id. -func (a API) DeleteCloudAccountExocomputeConfigurations(ctx context.Context, id uuid.UUID) error { - a.log.Print(log.Trace) +// ExoDeleteResult represents the result of deleting an Azure exocompute +// configuration. +type ExoDeleteResult struct { + FailIDs []uuid.UUID `json:"deletionFailedIds"` + SuccessIDs []uuid.UUID `json:"deletionSuccessIds"` +} - buf, err := a.GQL.Request(ctx, deleteAzureCloudAccountExocomputeConfigurationsQuery, struct { +func (ExoDeleteResult) DeleteQuery(configID uuid.UUID) (string, any) { + return deleteAzureCloudAccountExocomputeConfigurationsQuery, struct { IDs []uuid.UUID `json:"cloudAccountIds"` - }{IDs: []uuid.UUID{id}}) - if err != nil { - return fmt.Errorf("failed to request deleteAzureCloudAccountExocomputeConfigurations: %w", err) + }{IDs: []uuid.UUID{configID}} +} + +func (r ExoDeleteResult) Validate() (uuid.UUID, error) { + if len(r.FailIDs) > 0 { + return uuid.Nil, errors.New("expected no delete failures") } - a.log.Printf(log.Debug, "deleteAzureCloudAccountExocomputeConfigurations(%q): %s", id, string(buf)) - - var payload struct { - Data struct { - Result struct { - FailIDs []uuid.UUID `json:"deletionFailedIds"` - SuccessIDs []uuid.UUID `json:"deletionSuccessIds"` - } `json:"result"` - } `json:"data"` + if len(r.SuccessIDs) != 1 { + return uuid.Nil, errors.New("expected a single delete result") } - if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal deleteAzureCloudAccountExocomputeConfigurations: %v", err) + + return r.SuccessIDs[0], nil +} + +// ExoMapResult represents the result of mapping an Azure application cloud +// account to an Azure host cloud account. +type ExoMapResult struct { + Success bool `json:"isSuccess"` +} + +func (ExoMapResult) MapQuery(hostCloudAccountID, appCloudAccountID uuid.UUID) (string, any) { + return mapAzureCloudAccountExocomputeSubscriptionQuery, struct { + HostCloudAccountID uuid.UUID `json:"exocomputeCloudAccountId"` + AppCloudAccountIDs []uuid.UUID `json:"cloudAccountIds"` + }{HostCloudAccountID: hostCloudAccountID, AppCloudAccountIDs: []uuid.UUID{appCloudAccountID}} +} + +func (r ExoMapResult) Validate() error { + if !r.Success { + return errors.New("failed to map application cloud account") } - if ids := payload.Data.Result.SuccessIDs; len(ids) == 1 && ids[0] == id { - return nil + + return nil +} + +// ExoUnmapResult represents the result of unmapping an Azure application cloud +// account. +type ExoUnmapResult struct { + Success bool `json:"isSuccess"` +} + +func (ExoUnmapResult) UnmapQuery(appCloudAccountID uuid.UUID) (string, any) { + return unmapAzureCloudAccountExocomputeSubscriptionQuery, struct { + AppCloudAccountIDs []uuid.UUID `json:"cloudAccountIds"` + }{AppCloudAccountIDs: []uuid.UUID{appCloudAccountID}} +} + +func (r ExoUnmapResult) Validate() error { + if !r.Success { + return errors.New("failed to unmap application cloud account") } - return errors.New("delete exocompute config failed") + return nil } diff --git a/pkg/polaris/graphql/azure/native.go b/pkg/polaris/graphql/azure/native.go index 34d00994..35e2167e 100644 --- a/pkg/polaris/graphql/azure/native.go +++ b/pkg/polaris/graphql/azure/native.go @@ -91,10 +91,21 @@ func (a API) NativeSubscriptions(ctx context.Context, filter string) ([]NativeSu return subscriptions, nil } -// StartDisableNativeSubscriptionProtectionJob starts a task chain job to -// disable the native subscription with the specified RSC native subscription -// id. If deleteSnapshots is true the snapshots are deleted. Returns the RSC -// task chain id. +// Deprecated: no replacement. +type ProtectionFeature string + +const ( + // Deprecated: no replacement. + SQLDB ProtectionFeature = "SQL_DB" + + // Deprecated: no replacement. + SQLMI ProtectionFeature = "SQL_MI" + + // Deprecated: no replacement. + VM ProtectionFeature = "VM" +) + +// Deprecated: use StartDisableCloudAccountJob instead. func (a API) StartDisableNativeSubscriptionProtectionJob(ctx context.Context, id uuid.UUID, feature ProtectionFeature, deleteSnapshots bool) (uuid.UUID, error) { a.log.Print(log.Trace) diff --git a/pkg/polaris/graphql/azure/queries.go b/pkg/polaris/graphql/azure/queries.go index df07e9d0..ab8049f1 100644 --- a/pkg/polaris/graphql/azure/queries.go +++ b/pkg/polaris/graphql/azure/queries.go @@ -67,7 +67,10 @@ var allAzureCloudAccountTenantsQuery = `query SdkGolangAllAzureCloudAccountTenan result: allAzureCloudAccountTenants(feature: $feature, includeSubscriptionDetails: $includeSubscriptionDetails) { cloudType azureCloudAccountTenantRubrikId + clientId + appName domainName + subscriptionCount subscriptions { id name @@ -76,6 +79,20 @@ var allAzureCloudAccountTenantsQuery = `query SdkGolangAllAzureCloudAccountTenan feature status regions + resourceGroup { + name + nativeId + region + tags { + key + value + } + } + userAssignedManagedIdentity { + name + nativeId + principalId + } } } } @@ -96,8 +113,16 @@ var allAzureExocomputeConfigsInAccountQuery = `query SdkGolangAllAzureExocompute } configs { configUuid + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId + } isRscManaged message + podOverlayNetworkCidr + podSubnetNativeId region subnetNativeId } @@ -110,35 +135,63 @@ var allAzureExocomputeConfigsInAccountQuery = `query SdkGolangAllAzureExocompute } }` +// allTargetMappings GraphQL query +var allTargetMappingsQuery = `query SdkGolangAllTargetMappings($filter: [TargetMappingFilterInput!]) { + result: allTargetMappings(sortBy: NAME, sortOrder: ASC, filter: $filter) { + id + name + groupType + targetType + connectionStatus { + status + } + targetTemplate { + ... on AzureTargetTemplate { + cloudAccount { + cloudAccountId + } + cloudNativeCompanion { + cloudNativeLocTemplateType + cmkInfo { + keyName + keyVaultName + region + } + redundancy + storageAccountRegion + storageAccountTags { + key + value + } + storageTier + subscriptionNativeId + } + containerNamePrefix + storageAccountName + } + } + } +}` + // azureCloudAccountPermissionConfig GraphQL query var azureCloudAccountPermissionConfigQuery = `query SdkGolangAzureCloudAccountPermissionConfig($feature: CloudAccountFeature!) { - azureCloudAccountPermissionConfig(feature: $feature) { + result: azureCloudAccountPermissionConfig(feature: $feature) { permissionVersion - rolePermissions { + permissionsGroupVersions { + permissionsGroup + version + } + resourceGroupRolePermissions { excludedActions excludedDataActions includedActions includedDataActions } - } -}` - -// azureCloudAccountTenant GraphQL query -var azureCloudAccountTenantQuery = `query SdkGolangAzureCloudAccountTenant($tenantId: UUID!, $feature: CloudAccountFeature!, $subscriptionSearchText: String!) { - result: azureCloudAccountTenant(tenantId: $tenantId, feature: $feature, subscriptionSearchText: $subscriptionSearchText, subscriptionStatusFilters: []) { - cloudType - azureCloudAccountTenantRubrikId - clientId - domainName - subscriptions { - id - name - nativeId - featureDetail { - feature - regions - status - } + rolePermissions { + excludedActions + excludedDataActions + includedActions + includedDataActions } } }` @@ -175,6 +228,39 @@ var azureNativeSubscriptionsQuery = `query SdkGolangAzureNativeSubscriptions($af } }` +// createCloudNativeAzureStorageSetting GraphQL query +var createCloudNativeAzureStorageSettingQuery = `mutation SdkGolangCreateCloudNativeAzureStorageSetting( + $cloudAccountId: UUID!, + $cloudNativeLocTemplateType: CloudNativeLocTemplateType!, + $cmkInfo: [AzureCmkInput!], + $containerName: String!, + $name: String!, + $redundancy: AzureRedundancy!, + $storageTier: AzureStorageTier!, + $subscriptionNativeId: String! + $storageAccountName: String!, + $storageAccountRegion: AzureRegion, + $storageAccountTags: TagsInput, +) { + result: createCloudNativeAzureStorageSetting(input: { + cloudAccountId: $cloudAccountId, + cloudNativeLocTemplateType: $cloudNativeLocTemplateType, + cmkInfo: $cmkInfo, + containerName: $containerName, + name: $name, + redundancy: $redundancy, + storageTier: $storageTier, + subscriptionNativeId: $subscriptionNativeId + storageAccountName: $storageAccountName, + storageAccountRegion: $storageAccountRegion, + storageAccountTags: $storageAccountTags, + }) { + targetMapping { + id + } + } +}` + // deleteAzureCloudAccountExocomputeConfigurations GraphQL query var deleteAzureCloudAccountExocomputeConfigurationsQuery = `mutation SdkGolangDeleteAzureCloudAccountExocomputeConfigurations($cloudAccountIds: [UUID!]!) { result: deleteAzureCloudAccountExocomputeConfigurations(input: { @@ -199,6 +285,16 @@ var deleteAzureCloudAccountWithoutOauthQuery = `mutation SdkGolangDeleteAzureClo } }` +// mapAzureCloudAccountExocomputeSubscription GraphQL query +var mapAzureCloudAccountExocomputeSubscriptionQuery = `mutation SdkGolangMapAzureCloudAccountExocomputeSubscription($exocomputeCloudAccountId: UUID!, $cloudAccountIds: [UUID!]!) { + result: mapAzureCloudAccountExocomputeSubscription(input: { + exocomputeCloudAccountId: $exocomputeCloudAccountId, + cloudAccountIds: $cloudAccountIds + }) { + isSuccess + } +}` + // setAzureCloudAccountCustomerAppCredentials GraphQL query var setAzureCloudAccountCustomerAppCredentialsQuery = `mutation SdkGolangSetAzureCloudAccountCustomerAppCredentials($azureCloudType: AzureCloudType!, $appId: String!, $appName: String, $appSecretKey: String!, $appTenantId: String, $tenantDomainName: String, $shouldReplace: Boolean!) { result: setAzureCloudAccountCustomerAppCredentials(input: { @@ -238,6 +334,15 @@ var startDisableAzureNativeSubscriptionProtectionJobQuery = `mutation SdkGolangS } }` +// unmapAzureCloudAccountExocomputeSubscription GraphQL query +var unmapAzureCloudAccountExocomputeSubscriptionQuery = `mutation SdkGolangUnmapAzureCloudAccountExocomputeSubscription($cloudAccountIds: [UUID!]!) { + result: unmapAzureCloudAccountExocomputeSubscription(input: { + cloudAccountIds: $cloudAccountIds + }) { + isSuccess + } +}` + // updateAzureCloudAccount GraphQL query var updateAzureCloudAccountQuery = `mutation SdkGolangUpdateAzureCloudAccount($features: [CloudAccountFeature!]!, $regionsToAdd: [AzureCloudAccountRegion!], $regionsToRemove: [AzureCloudAccountRegion!], $subscriptions: [AzureCloudAccountSubscriptionInput!]!) { result: updateAzureCloudAccount(input: { @@ -253,6 +358,27 @@ var updateAzureCloudAccountQuery = `mutation SdkGolangUpdateAzureCloudAccount($f } }` +// updateCloudNativeAzureStorageSetting GraphQL query +var updateCloudNativeAzureStorageSettingQuery = `mutation SdkGolangUpdateCloudNativeAzureStorageSetting( + $id: UUID!, + $name: String!, + $storageTier: AzureStorageTier!, + $storageAccountTags: TagsInput!, + $cmkInfo: [AzureCmkInput!], +) { + result: updateCloudNativeAzureStorageSetting(input: { + id: $id, + name: $name, + storageTier: $storageTier, + storageAccountTags: $storageAccountTags, + cmkInfo: $cmkInfo, + }) { + targetMapping { + id + } + } +}` + // upgradeAzureCloudAccountPermissionsWithoutOauth GraphQL query var upgradeAzureCloudAccountPermissionsWithoutOauthQuery = `mutation SdkGolangUpgradeAzureCloudAccountPermissionsWithoutOauth($cloudAccountId: UUID!, $feature: CloudAccountFeature!) { result: upgradeAzureCloudAccountPermissionsWithoutOauth(input: { diff --git a/pkg/polaris/graphql/azure/queries/all_azure_cloud_account_tenants.graphql b/pkg/polaris/graphql/azure/queries/all_azure_cloud_account_tenants.graphql index 152d01f5..fa97536c 100644 --- a/pkg/polaris/graphql/azure/queries/all_azure_cloud_account_tenants.graphql +++ b/pkg/polaris/graphql/azure/queries/all_azure_cloud_account_tenants.graphql @@ -2,7 +2,10 @@ query RubrikPolarisSDKRequest($feature: CloudAccountFeature!, $includeSubscripti result: allAzureCloudAccountTenants(feature: $feature, includeSubscriptionDetails: $includeSubscriptionDetails) { cloudType azureCloudAccountTenantRubrikId + clientId + appName domainName + subscriptionCount subscriptions { id name @@ -11,6 +14,20 @@ query RubrikPolarisSDKRequest($feature: CloudAccountFeature!, $includeSubscripti feature status regions + resourceGroup { + name + nativeId + region + tags { + key + value + } + } + userAssignedManagedIdentity { + name + nativeId + principalId + } } } } diff --git a/pkg/polaris/graphql/azure/queries/all_azure_exocompute_configs_in_account.graphql b/pkg/polaris/graphql/azure/queries/all_azure_exocompute_configs_in_account.graphql index ab5ed3a0..e313c29b 100644 --- a/pkg/polaris/graphql/azure/queries/all_azure_exocompute_configs_in_account.graphql +++ b/pkg/polaris/graphql/azure/queries/all_azure_exocompute_configs_in_account.graphql @@ -12,8 +12,16 @@ query RubrikPolarisSDKRequest($cloudAccountIDs: [UUID!], $azureExocomputeSearchQ } configs { configUuid + healthCheckStatus { + failureReason + lastUpdatedAt + status + taskchainId + } isRscManaged message + podOverlayNetworkCidr + podSubnetNativeId region subnetNativeId } diff --git a/pkg/polaris/graphql/azure/queries/all_target_mappings.graphql b/pkg/polaris/graphql/azure/queries/all_target_mappings.graphql new file mode 100644 index 00000000..016bb23a --- /dev/null +++ b/pkg/polaris/graphql/azure/queries/all_target_mappings.graphql @@ -0,0 +1,36 @@ +query RubrikPolarisSDKRequest($filter: [TargetMappingFilterInput!]) { + result: allTargetMappings(sortBy: NAME, sortOrder: ASC, filter: $filter) { + id + name + groupType + targetType + connectionStatus { + status + } + targetTemplate { + ... on AzureTargetTemplate { + cloudAccount { + cloudAccountId + } + cloudNativeCompanion { + cloudNativeLocTemplateType + cmkInfo { + keyName + keyVaultName + region + } + redundancy + storageAccountRegion + storageAccountTags { + key + value + } + storageTier + subscriptionNativeId + } + containerNamePrefix + storageAccountName + } + } + } +} diff --git a/pkg/polaris/graphql/azure/queries/azure_cloud_account_permission_config.graphql b/pkg/polaris/graphql/azure/queries/azure_cloud_account_permission_config.graphql index e018bcd5..30c329de 100644 --- a/pkg/polaris/graphql/azure/queries/azure_cloud_account_permission_config.graphql +++ b/pkg/polaris/graphql/azure/queries/azure_cloud_account_permission_config.graphql @@ -1,6 +1,16 @@ query RubrikPolarisSDKRequest($feature: CloudAccountFeature!) { - azureCloudAccountPermissionConfig(feature: $feature) { + result: azureCloudAccountPermissionConfig(feature: $feature) { permissionVersion + permissionsGroupVersions { + permissionsGroup + version + } + resourceGroupRolePermissions { + excludedActions + excludedDataActions + includedActions + includedDataActions + } rolePermissions { excludedActions excludedDataActions diff --git a/pkg/polaris/graphql/azure/queries/azure_cloud_account_tenant.graphql b/pkg/polaris/graphql/azure/queries/azure_cloud_account_tenant.graphql deleted file mode 100644 index 0c82a941..00000000 --- a/pkg/polaris/graphql/azure/queries/azure_cloud_account_tenant.graphql +++ /dev/null @@ -1,18 +0,0 @@ -query RubrikPolarisSDKRequest($tenantId: UUID!, $feature: CloudAccountFeature!, $subscriptionSearchText: String!) { - result: azureCloudAccountTenant(tenantId: $tenantId, feature: $feature, subscriptionSearchText: $subscriptionSearchText, subscriptionStatusFilters: []) { - cloudType - azureCloudAccountTenantRubrikId - clientId - domainName - subscriptions { - id - name - nativeId - featureDetail { - feature - regions - status - } - } - } -} diff --git a/pkg/polaris/graphql/azure/queries/create_cloud_native_azure_storage_setting.graphql b/pkg/polaris/graphql/azure/queries/create_cloud_native_azure_storage_setting.graphql new file mode 100644 index 00000000..9db50b6c --- /dev/null +++ b/pkg/polaris/graphql/azure/queries/create_cloud_native_azure_storage_setting.graphql @@ -0,0 +1,31 @@ +mutation RubrikPolarisSDKRequest( + $cloudAccountId: UUID!, + $cloudNativeLocTemplateType: CloudNativeLocTemplateType!, + $cmkInfo: [AzureCmkInput!], + $containerName: String!, + $name: String!, + $redundancy: AzureRedundancy!, + $storageTier: AzureStorageTier!, + $subscriptionNativeId: String! + $storageAccountName: String!, + $storageAccountRegion: AzureRegion, + $storageAccountTags: TagsInput, +) { + result: createCloudNativeAzureStorageSetting(input: { + cloudAccountId: $cloudAccountId, + cloudNativeLocTemplateType: $cloudNativeLocTemplateType, + cmkInfo: $cmkInfo, + containerName: $containerName, + name: $name, + redundancy: $redundancy, + storageTier: $storageTier, + subscriptionNativeId: $subscriptionNativeId + storageAccountName: $storageAccountName, + storageAccountRegion: $storageAccountRegion, + storageAccountTags: $storageAccountTags, + }) { + targetMapping { + id + } + } +} diff --git a/pkg/polaris/graphql/azure/queries/map_azure_cloud_account_exocompute_subscription.graphql b/pkg/polaris/graphql/azure/queries/map_azure_cloud_account_exocompute_subscription.graphql new file mode 100644 index 00000000..17c25e51 --- /dev/null +++ b/pkg/polaris/graphql/azure/queries/map_azure_cloud_account_exocompute_subscription.graphql @@ -0,0 +1,8 @@ +mutation RubrikPolarisSDKRequest($exocomputeCloudAccountId: UUID!, $cloudAccountIds: [UUID!]!) { + result: mapAzureCloudAccountExocomputeSubscription(input: { + exocomputeCloudAccountId: $exocomputeCloudAccountId, + cloudAccountIds: $cloudAccountIds + }) { + isSuccess + } +} diff --git a/pkg/polaris/graphql/azure/queries/unmap_azure_cloud_account_exocompute_subscription.graphql b/pkg/polaris/graphql/azure/queries/unmap_azure_cloud_account_exocompute_subscription.graphql new file mode 100644 index 00000000..b96db3a6 --- /dev/null +++ b/pkg/polaris/graphql/azure/queries/unmap_azure_cloud_account_exocompute_subscription.graphql @@ -0,0 +1,7 @@ +mutation RubrikPolarisSDKRequest($cloudAccountIds: [UUID!]!) { + result: unmapAzureCloudAccountExocomputeSubscription(input: { + cloudAccountIds: $cloudAccountIds + }) { + isSuccess + } +} diff --git a/pkg/polaris/graphql/azure/queries/update_cloud_native_azure_storage_setting.graphql b/pkg/polaris/graphql/azure/queries/update_cloud_native_azure_storage_setting.graphql new file mode 100644 index 00000000..19baf0f4 --- /dev/null +++ b/pkg/polaris/graphql/azure/queries/update_cloud_native_azure_storage_setting.graphql @@ -0,0 +1,19 @@ +mutation RubrikPolarisSDKRequest( + $id: UUID!, + $name: String!, + $storageTier: AzureStorageTier!, + $storageAccountTags: TagsInput!, + $cmkInfo: [AzureCmkInput!], +) { + result: updateCloudNativeAzureStorageSetting(input: { + id: $id, + name: $name, + storageTier: $storageTier, + storageAccountTags: $storageAccountTags, + cmkInfo: $cmkInfo, + }) { + targetMapping { + id + } + } +} diff --git a/pkg/polaris/graphql/azure/regions.go b/pkg/polaris/graphql/azure/regions.go new file mode 100644 index 00000000..531a61bf --- /dev/null +++ b/pkg/polaris/graphql/azure/regions.go @@ -0,0 +1,878 @@ +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE.package azure + +package azure + +import ( + "encoding/json" + "errors" + "fmt" +) + +const ( + RegionUnknown Region = iota + RegionAustraliaCentral + RegionAustraliaCentral2 + RegionAustraliaEast + RegionAustraliaSoutheast + RegionBrazilSouth + RegionBrazilSoutheast + RegionCanadaCentral + RegionCanadaEast + RegionCentralIndia + RegionCentralUS + RegionChinaEast + RegionChinaEast2 + RegionChinaNorth + RegionChinaNorth2 + RegionEastAsia + RegionEastUS + RegionEastUS2 + RegionFranceCentral + RegionFranceSouth + RegionGermanyNorth + RegionGermanyWestCentral + RegionIsraelCentral + RegionItalyNorth + RegionJapanEast + RegionJapanWest + RegionJioIndiaCentral + RegionJioIndiaWest + RegionKoreaCentral + RegionKoreaSouth + RegionMexicoCentral + RegionNorthCentralUS + RegionNorthEurope + RegionNorwayEast + RegionNorwayWest + RegionPolandCentral + RegionQatarCentral + RegionSouthAfricaNorth + RegionSouthAfricaWest + RegionSouthCentralUS + RegionSoutheastAsia + RegionSouthIndia + RegionSwedenCentral + RegionSwitzerlandNorth + RegionSwitzerlandWest + RegionUAECentral + RegionUAENorth + RegionUKSouth + RegionUKWest + RegionUSDoDCentral + RegionUSDoDEast + RegionUSGovArizona + RegionUSGovTexas + RegionUSGovVirginia + RegionWestCentralUS + RegionWestEurope + RegionWestIndia + RegionWestUS + RegionWestUS2 + RegionWestUS3 +) + +// Region represents an Azure region in RSC. When reading a Region from a JSON +// document or writing a Region to a JSON document, use one of the specialized +// region enum types, to guarantee that the correct enum value is used. +type Region int + +// Name returns the name of the region. +func (region Region) Name() string { + return regionInfoMap[region].name +} + +// DisplayName returns the display name of the region. +func (region Region) DisplayName() string { + return regionInfoMap[region].displayName +} + +// RegionalDisplayName returns the regional display name of the region. +func (region Region) RegionalDisplayName() string { + return regionInfoMap[region].regionalDisplayName +} + +// ToRegion returns the Region. This is provided for region enum types which +// embeds the Region type. +func (region Region) ToRegion() Region { + return region +} + +// ToCloudAccountRegionEnum returns the RSC AzureCloudAccountRegion enum for +// the region. +func (region Region) ToCloudAccountRegionEnum() CloudAccountRegionEnum { + return CloudAccountRegionEnum{Region: region} +} + +// ToNativeRegionEnum returns the RSC AzureNativeRegion enum for the region. +func (region Region) ToNativeRegionEnum() NativeRegionEnum { + return NativeRegionEnum{Region: region} +} + +// ToRegionEnum returns the RSC AzureRegion enum for the region. +func (region Region) ToRegionEnum() RegionEnum { + return RegionEnum{Region: region} +} + +// String returns the name of the region. +func (region Region) String() string { + return region.Name() +} + +const ( + FromAny = iota // Parse the value as any of the below formats. + FromCloudAccountRegionEnum // Parse the value as an AzureCloudAccountRegion enum value. + FromDisplayName // Parse the value as a region display name. + FromName // Parse the value as a region name. + FromNativeRegionEnum // Parse the value as an AzureNativeRegion enum value. + FromRegionalDisplayName // Parse the value as a region regional display name. + FromRegionEnum // Parse the value as an AzureRegion enum value. +) + +// RegionFrom parses the value as a region identifier in the specified format. +// If the value isn't recognized, RegionUnknown is returned. +func RegionFrom(value string, valueFormat int) Region { + for r, info := range regionInfoMap { + switch { + case (valueFormat == FromAny || valueFormat == FromName) && info.name == value: + return r + case (valueFormat == FromAny || valueFormat == FromCloudAccountRegionEnum) && info.cloudAccountRegionEnum == value: + return r + case (valueFormat == FromAny || valueFormat == FromNativeRegionEnum) && info.nativeRegionEnum == value: + return r + case (valueFormat == FromAny || valueFormat == FromRegionEnum) && info.regionEnum == value: + return r + case (valueFormat == FromAny || valueFormat == FromDisplayName) && info.displayName == value: + return r + case (valueFormat == FromAny || valueFormat == FromRegionalDisplayName) && info.regionalDisplayName == value: + return r + } + } + + return RegionUnknown +} + +// RegionFromAny parses the value as any region identifier that matches. +func RegionFromAny(value string) Region { + return RegionFrom(value, FromAny) +} + +// RegionFromName parses the value as a region name. +func RegionFromName(value string) Region { + return RegionFrom(value, FromName) +} + +// RegionFromDisplayName parses the value as a region display name. +func RegionFromDisplayName(value string) Region { + return RegionFrom(value, FromDisplayName) +} + +// RegionFromRegionalDisplayName parses the value as a region regional display +// name. +func RegionFromRegionalDisplayName(value string) Region { + return RegionFrom(value, FromRegionalDisplayName) +} + +// RegionFromCloudAccountRegionEnum parses the value as an +// AzureCloudAccountRegion enum value. +func RegionFromCloudAccountRegionEnum(value string) Region { + return RegionFrom(value, FromCloudAccountRegionEnum) +} + +// RegionFromNativeRegionEnum parses the value as an AzureNativeRegion enum. +func RegionFromNativeRegionEnum(value string) Region { + return RegionFrom(value, FromNativeRegionEnum) +} + +// RegionFromRegionEnum parses the value as an AzureRegion enum value. +func RegionFromRegionEnum(value string) Region { + return RegionFrom(value, FromRegionEnum) +} + +// RegionEnum represents the GraphQL AzureRegion enum type. +type RegionEnum struct{ Region } + +// MarshalJSON returns the region as a JSON string. +func (region *RegionEnum) MarshalJSON() ([]byte, error) { + return json.Marshal(regionInfoMap[region.Region].regionEnum) +} + +// UnmarshalJSON parses the region from a JSON string. +func (region *RegionEnum) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + region.Region = RegionFromRegionEnum(s) + return nil +} + +// CloudAccountRegionEnum represents the GraphQL AzureCloudAccountRegion enum +// type. +type CloudAccountRegionEnum struct{ Region } + +// MarshalJSON returns the region as a JSON string. +func (region *CloudAccountRegionEnum) MarshalJSON() ([]byte, error) { + return json.Marshal(regionInfoMap[region.Region].cloudAccountRegionEnum) +} + +// UnmarshalJSON parses the region from a JSON string. +func (region *CloudAccountRegionEnum) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + region.Region = RegionFromCloudAccountRegionEnum(s) + return nil +} + +// NativeRegionEnum represents the GraphQL AzureNativeRegion enum type. +type NativeRegionEnum struct{ Region } + +// MarshalJSON returns the region as a JSON string. +func (region *NativeRegionEnum) MarshalJSON() ([]byte, error) { + return json.Marshal(regionInfoMap[region.Region].nativeRegionEnum) +} + +// UnmarshalJSON parses the region from a JSON string. +func (region *NativeRegionEnum) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + region.Region = RegionFromNativeRegionEnum(s) + return nil +} + +var regionInfoMap = map[Region]struct { + name string + displayName string + regionalDisplayName string + regionEnum string + cloudAccountRegionEnum string + nativeRegionEnum string +}{ + RegionUnknown: { + name: "", + displayName: "", + regionalDisplayName: "", + regionEnum: "UNKNOWN_AZURE_REGION", + cloudAccountRegionEnum: "UNKNOWN_AZURE_REGION", + nativeRegionEnum: "NOT_SPECIFIED", + }, + RegionAustraliaCentral: { + name: "australiacentral", + displayName: "Australia Central", + regionalDisplayName: "(Asia Pacific) Australia Central", + regionEnum: "AUSTRALIA_CENTRAL", + cloudAccountRegionEnum: "AUSTRALIACENTRAL", + nativeRegionEnum: "AUSTRALIA_CENTRAL", + }, + RegionAustraliaCentral2: { + name: "australiacentral2", + displayName: "Australia Central 2", + regionalDisplayName: "(Asia Pacific) Australia Central 2", + regionEnum: "AUSTRALIA_CENTRAL2", + cloudAccountRegionEnum: "AUSTRALIACENTRAL2", + nativeRegionEnum: "AUSTRALIA_CENTRAL2", + }, + RegionAustraliaEast: { + name: "australiaeast", + displayName: "Australia East", + regionalDisplayName: "(Asia Pacific) Australia East", + regionEnum: "AUSTRALIA_EAST", + cloudAccountRegionEnum: "AUSTRALIAEAST", + nativeRegionEnum: "AUSTRALIA_EAST", + }, + RegionAustraliaSoutheast: { + name: "australiasoutheast", + displayName: "Australia Southeast", + regionalDisplayName: "(Asia Pacific) Australia Southeast", + regionEnum: "AUSTRALIA_SOUTHEAST", + cloudAccountRegionEnum: "AUSTRALIASOUTHEAST", + nativeRegionEnum: "AUSTRALIA_SOUTHEAST", + }, + RegionBrazilSouth: { + name: "brazilsouth", + displayName: "Brazil South", + regionalDisplayName: "(South America) Brazil South", + regionEnum: "BRAZIL_SOUTH", + cloudAccountRegionEnum: "BRAZILSOUTH", + nativeRegionEnum: "BRAZIL_SOUTH", + }, + RegionBrazilSoutheast: { + name: "brazilsoutheast", + displayName: "Brazil Southeast", + regionalDisplayName: "(South America) Brazil Southeast", + regionEnum: "BRAZIL_SOUTHEAST", + cloudAccountRegionEnum: "BRAZILSOUTHEAST", + nativeRegionEnum: "BRAZIL_SOUTHEAST", + }, + RegionCanadaCentral: { + name: "canadacentral", + displayName: "Canada Central", + regionalDisplayName: "(Canada) Canada Central", + regionEnum: "CANADA_CENTRAL", + cloudAccountRegionEnum: "CANADACENTRAL", + nativeRegionEnum: "CANADA_CENTRAL", + }, + RegionCanadaEast: { + name: "canadaeast", + displayName: "Canada East", + regionalDisplayName: "(Canada) Canada East", + regionEnum: "CANADA_EAST", + cloudAccountRegionEnum: "CANADAEAST", + nativeRegionEnum: "CANADA_EAST", + }, + RegionCentralIndia: { + name: "centralindia", + displayName: "Central India", + regionalDisplayName: "(Asia Pacific) Central India", + regionEnum: "ISRAEL_CENTRAL", + cloudAccountRegionEnum: "CENTRALINDIA", + nativeRegionEnum: "CENTRAL_INDIA", + }, + RegionCentralUS: { + name: "centralus", + displayName: "Central US", + regionalDisplayName: "(US) Central US", + regionEnum: "US_CENTRAL", + cloudAccountRegionEnum: "CENTRALUS", + nativeRegionEnum: "CENTRAL_US", + }, + RegionChinaEast: { + name: "chinaeast", + displayName: "China East", + regionalDisplayName: "(China) China East", + regionEnum: "CHINA_EAST", + cloudAccountRegionEnum: "CHINAEAST", + nativeRegionEnum: "CHINA_EAST", + }, + RegionChinaEast2: { + name: "chinaeast2", + displayName: "China East 2", + regionalDisplayName: "(China) China East 2", + regionEnum: "CHINA_EAST2", + cloudAccountRegionEnum: "CHINAEAST2", + nativeRegionEnum: "CHINA_EAST2", + }, + RegionChinaNorth: { + name: "chinanorth", + displayName: "China North", + regionalDisplayName: "(China) China North", + regionEnum: "CHINA_NORTH", + cloudAccountRegionEnum: "CHINANORTH", + nativeRegionEnum: "CHINA_NORTH", + }, + RegionChinaNorth2: { + name: "chinanorth2", + displayName: "China North 2", + regionalDisplayName: "(China) China North 2", + regionEnum: "CHINA_NORTH2", + cloudAccountRegionEnum: "CHINANORTH2", + nativeRegionEnum: "CHINA_NORTH2", + }, + RegionEastAsia: { + name: "eastasia", + displayName: "East Asia", + regionalDisplayName: "(Asia Pacific) East Asia", + regionEnum: "ASIA_EAST", + cloudAccountRegionEnum: "EASTASIA", + nativeRegionEnum: "EAST_ASIA", + }, + RegionEastUS: { + name: "eastus", + displayName: "East US", + regionalDisplayName: "(US) East US", + regionEnum: "US_EAST", + cloudAccountRegionEnum: "EASTUS", + nativeRegionEnum: "EAST_US", + }, + RegionEastUS2: { + name: "eastus2", + displayName: "East US 2", + regionalDisplayName: "(US) East US 2", + regionEnum: "US_EAST2", + cloudAccountRegionEnum: "EASTUS2", + nativeRegionEnum: "EAST_US2", + }, + RegionFranceCentral: { + name: "francecentral", + displayName: "France Central", + regionalDisplayName: "(Europe) France Central", + regionEnum: "FRANCE_CENTRAL", + cloudAccountRegionEnum: "FRANCECENTRAL", + nativeRegionEnum: "FRANCE_CENTRAL", + }, + RegionFranceSouth: { + name: "francesouth", + displayName: "France South", + regionalDisplayName: "(Europe) France South", + regionEnum: "FRANCE_SOUTH", + cloudAccountRegionEnum: "FRANCESOUTH", + nativeRegionEnum: "FRANCE_SOUTH", + }, + RegionGermanyNorth: { + name: "germanynorth", + displayName: "Germany North", + regionalDisplayName: "(Europe) Germany North", + regionEnum: "GERMANY_NORTH", + cloudAccountRegionEnum: "GERMANYNORTH", + nativeRegionEnum: "GERMANY_NORTH", + }, + RegionGermanyWestCentral: { + name: "germanywestcentral", + displayName: "Germany West Central", + regionalDisplayName: "(Europe) Germany West Central", + regionEnum: "GERMANY_WEST_CENTRAL", + cloudAccountRegionEnum: "GERMANYWESTCENTRAL", + nativeRegionEnum: "GERMANY_WEST_CENTRAL", + }, + RegionIsraelCentral: { + name: "israelcentral", + displayName: "Israel Central", + regionalDisplayName: "(Middle East) Israel Central", + regionEnum: "ISRAEL_CENTRAL", + cloudAccountRegionEnum: "ISRAELCENTRAL", + nativeRegionEnum: "ISRAEL_CENTRAL", + }, + RegionItalyNorth: { + name: "italynorth", + displayName: "Italy North", + regionalDisplayName: "(Europe) Italy North", + regionEnum: "ITALY_NORTH", + cloudAccountRegionEnum: "ITALYNORTH", + nativeRegionEnum: "ITALY_NORTH", + }, + RegionJapanEast: { + name: "japaneast", + displayName: "Japan East", + regionalDisplayName: "(Asia Pacific) Japan East", + regionEnum: "JAPAN_EAST", + cloudAccountRegionEnum: "JAPANEAST", + nativeRegionEnum: "JAPAN_EAST", + }, + RegionJapanWest: { + name: "japanwest", + displayName: "Japan West", + regionalDisplayName: "(Asia Pacific) Japan West", + regionEnum: "JAPAN_WEST", + cloudAccountRegionEnum: "JAPANWEST", + nativeRegionEnum: "JAPAN_WEST", + }, + RegionJioIndiaCentral: { + name: "jioindiacentral", + displayName: "Jio India Central", + regionalDisplayName: "(Asia Pacific) Jio India Central", + regionEnum: "JIO_INDIA_CENTRAL", + cloudAccountRegionEnum: "JIOINDIACENTRAL", + nativeRegionEnum: "JIO_INDIA_CENTRAL", + }, + RegionJioIndiaWest: { + name: "jioindiawest", + displayName: "Jio India West", + regionalDisplayName: "(Asia Pacific) Jio India West", + regionEnum: "JIO_INDIA_WEST", + cloudAccountRegionEnum: "JIOINDIAWEST", + nativeRegionEnum: "JIO_INDIA_WEST", + }, + RegionKoreaCentral: { + name: "koreacentral", + displayName: "Korea Central", + regionalDisplayName: "(Asia Pacific) Korea Central", + regionEnum: "KOREA_CENTRAL", + cloudAccountRegionEnum: "KOREACENTRAL", + nativeRegionEnum: "KOREA_CENTRAL", + }, + RegionKoreaSouth: { + name: "koreasouth", + displayName: "Korea South", + regionalDisplayName: "(Asia Pacific) Korea South", + regionEnum: "KOREA_SOUTH", + cloudAccountRegionEnum: "KOREASOUTH", + nativeRegionEnum: "KOREA_SOUTH", + }, + RegionMexicoCentral: { + name: "mexicocentral", + displayName: "Mexico Central", + regionalDisplayName: "(Mexico) Mexico Central", + regionEnum: "MEXICO_CENTRAL", + cloudAccountRegionEnum: "MEXICOCENTRAL", + nativeRegionEnum: "MEXICO_CENTRAL", + }, + RegionNorthCentralUS: { + name: "northcentralus", + displayName: "North Central US", + regionalDisplayName: "(US) North Central US", + regionEnum: "US_NORTH_CENTRAL", + cloudAccountRegionEnum: "NORTHCENTRALUS", + nativeRegionEnum: "NORTH_CENTRAL_US", + }, + RegionNorthEurope: { + name: "northeurope", + displayName: "North Europe", + regionalDisplayName: "(Europe) North Europe", + regionEnum: "EUROPE_NORTH", + cloudAccountRegionEnum: "NORTHEUROPE", + nativeRegionEnum: "NORTH_EUROPE", + }, + RegionNorwayEast: { + name: "norwayeast", + displayName: "Norway East", + regionalDisplayName: "(Europe) Norway East", + regionEnum: "NORWAY_EAST", + cloudAccountRegionEnum: "NORWAYEAST", + nativeRegionEnum: "NORWAY_EAST", + }, + RegionNorwayWest: { + name: "norwaywest", + displayName: "Norway West", + regionalDisplayName: "(Europe) Norway West", + regionEnum: "NORWAY_WEST", + cloudAccountRegionEnum: "NORWAYWEST", + nativeRegionEnum: "NORWAY_WEST", + }, + RegionPolandCentral: { + name: "polandcentral", + displayName: "Poland Central", + regionalDisplayName: "(Europe) Poland Central", + regionEnum: "POLAND_CENTRAL", + cloudAccountRegionEnum: "POLANDCENTRAL", + nativeRegionEnum: "POLAND_CENTRAL", + }, + RegionQatarCentral: { + name: "qatarcentral", + displayName: "Qatar Central", + regionalDisplayName: "(Middle East) Qatar Central", + regionEnum: "QATAR_CENTRAL", + cloudAccountRegionEnum: "QATARCENTRAL", + nativeRegionEnum: "QATAR_CENTRAL", + }, + RegionSouthAfricaNorth: { + name: "southafricanorth", + displayName: "South Africa North", + regionalDisplayName: "(Africa) South Africa North", + regionEnum: "SOUTH_AFRICA_NORTH", + cloudAccountRegionEnum: "SOUTHAFRICANORTH", + nativeRegionEnum: "SOUTH_AFRICA_NORTH", + }, + RegionSouthAfricaWest: { + name: "southafricawest", + displayName: "South Africa West", + regionalDisplayName: "(Africa) South Africa West", + regionEnum: "SOUTH_AFRICA_WEST", + cloudAccountRegionEnum: "SOUTHAFRICAWEST", + nativeRegionEnum: "SOUTH_AFRICA_WEST", + }, + RegionSouthCentralUS: { + name: "southcentralus", + displayName: "South Central US", + regionalDisplayName: "(US) South Central US", + regionEnum: "US_SOUTH_CENTRAL", + cloudAccountRegionEnum: "SOUTHCENTRALUS", + nativeRegionEnum: "SOUTH_CENTRAL_US", + }, + RegionSoutheastAsia: { + name: "southeastasia", + displayName: "Southeast Asia", + regionalDisplayName: "(Asia Pacific) Southeast Asia", + regionEnum: "ASIA_SOUTHEAST", + cloudAccountRegionEnum: "SOUTHEASTASIA", + nativeRegionEnum: "SOUTHEAST_ASIA", + }, + RegionSouthIndia: { + name: "southindia", + displayName: "South India", + regionalDisplayName: "(Asia Pacific) South India", + regionEnum: "INDIA_SOUTH", + cloudAccountRegionEnum: "SOUTHINDIA", + nativeRegionEnum: "SOUTH_INDIA", + }, + RegionSwedenCentral: { + name: "swedencentral", + displayName: "Sweden Central", + regionalDisplayName: "(Europe) Sweden Central", + regionEnum: "SWEDEN_CENTRAL", + cloudAccountRegionEnum: "SWEDENCENTRAL", + nativeRegionEnum: "SWEDEN_CENTRAL", + }, + RegionSwitzerlandNorth: { + name: "switzerlandnorth", + displayName: "Switzerland North", + regionalDisplayName: "(Europe) Switzerland North", + regionEnum: "SWITZERLAND_NORTH", + cloudAccountRegionEnum: "SWITZERLANDNORTH", + nativeRegionEnum: "SWITZERLAND_NORTH", + }, + RegionSwitzerlandWest: { + name: "switzerlandwest", + displayName: "Switzerland West", + regionalDisplayName: "(Europe) Switzerland West", + regionEnum: "SWITZERLAND_WEST", + cloudAccountRegionEnum: "SWITZERLANDWEST", + nativeRegionEnum: "SWITZERLAND_WEST", + }, + RegionUAECentral: { + name: "uaecentral", + displayName: "UAE Central", + regionalDisplayName: "(Middle East) UAE Central", + regionEnum: "UAE_CENTRAL", + cloudAccountRegionEnum: "UAECENTRAL", + nativeRegionEnum: "UAE_CENTRAL", + }, + RegionUAENorth: { + name: "uaenorth", + displayName: "UAE North", + regionalDisplayName: "(Middle East) UAE North", + regionEnum: "UAE_NORTH", + cloudAccountRegionEnum: "UAENORTH", + nativeRegionEnum: "UAE_NORTH", + }, + RegionUKSouth: { + name: "uksouth", + displayName: "UK South", + regionalDisplayName: "(Europe) UK South", + regionEnum: "UK_SOUTH", + cloudAccountRegionEnum: "UKSOUTH", + nativeRegionEnum: "UK_SOUTH", + }, + RegionUKWest: { + name: "ukwest", + displayName: "UK West", + regionalDisplayName: "(Europe) UK West", + regionEnum: "UK_WEST", + cloudAccountRegionEnum: "UKWEST", + nativeRegionEnum: "UK_WEST", + }, + RegionUSDoDCentral: { + name: "usdodcentral", + displayName: "US DoD Central", + regionalDisplayName: "(US Gov) US DoD Central", + regionEnum: "GOV_US_DOD_CENTRAL", + cloudAccountRegionEnum: "USDODCENTRAL", + nativeRegionEnum: "US_DOD_CENTRAL", + }, + RegionUSDoDEast: { + name: "usdodeast", + displayName: "US DoD East", + regionalDisplayName: "(US Gov) US DoD East", + regionEnum: "GOV_US_DOD_EAST", + cloudAccountRegionEnum: "USDODEAST", + nativeRegionEnum: "US_DOD_EAST", + }, + RegionUSGovArizona: { + name: "usgovarizona", + displayName: "US Gov Arizona", + regionalDisplayName: "(US Gov) US Gov Arizona", + regionEnum: "GOV_US_ARIZONA", + cloudAccountRegionEnum: "USGOVARIZONA", + nativeRegionEnum: "US_GOV_ARIZONA", + }, + RegionUSGovTexas: { + name: "usgovtexas", + displayName: "US Gov Texas", + regionalDisplayName: "(US Gov) US Gov Texas", + regionEnum: "GOV_US_TEXAS", + cloudAccountRegionEnum: "USGOVTEXAS", + nativeRegionEnum: "US_GOV_TEXAS", + }, + RegionUSGovVirginia: { + name: "usgovvirginia", + displayName: "US Gov Virginia", + regionalDisplayName: "(US Gov) US Gov Virginia", + regionEnum: "GOV_US_VIRGINIA", + cloudAccountRegionEnum: "USGOVVIRGINIA", + nativeRegionEnum: "US_GOV_VIRGINIA", + }, + RegionWestCentralUS: { + name: "westcentralus", + displayName: "West Central US", + regionalDisplayName: "(US) West Central US", + regionEnum: "US_WEST_CENTRAL", + cloudAccountRegionEnum: "WESTCENTRALUS", + nativeRegionEnum: "WEST_CENTRAL_US", + }, + RegionWestEurope: { + name: "westeurope", + displayName: "West Europe", + regionalDisplayName: "(Europe) West Europe", + regionEnum: "EUROPE_WEST", + cloudAccountRegionEnum: "WESTEUROPE", + nativeRegionEnum: "WEST_EUROPE", + }, + RegionWestIndia: { + name: "westindia", + displayName: "West India", + regionalDisplayName: "(Asia Pacific) West India", + regionEnum: "INDIA_WEST", + cloudAccountRegionEnum: "WESTINDIA", + nativeRegionEnum: "WEST_INDIA", + }, + RegionWestUS: { + name: "westus", + displayName: "West US", + regionalDisplayName: "(US) West US", + regionEnum: "US_WEST", + cloudAccountRegionEnum: "WESTUS", + nativeRegionEnum: "WEST_US", + }, + RegionWestUS2: { + name: "westus2", + displayName: "West US 2", + regionalDisplayName: "(US) West US 2", + regionEnum: "US_WEST2", + cloudAccountRegionEnum: "WESTUS2", + nativeRegionEnum: "WEST_US2", + }, + RegionWestUS3: { + name: "westus3", + displayName: "West US 3", + regionalDisplayName: "(US) West US 3", + regionEnum: "WEST_US3", + cloudAccountRegionEnum: "WESTUS3", + nativeRegionEnum: "WEST_US3", + }, +} + +// Deprecated: use Region.Name. +func FormatRegion(region Region) string { + return region.Name() +} + +// Deprecated: no replacement. +func FormatRegions(regions []Region) []string { + regs := make([]string, 0, len(regions)) + for _, region := range regions { + regs = append(regs, region.Name()) + } + + return regs +} + +// Deprecated: no replacement. +var validRegions = map[Region]struct{}{ + RegionAustraliaCentral: {}, + RegionAustraliaCentral2: {}, + RegionAustraliaEast: {}, + RegionAustraliaSoutheast: {}, + RegionBrazilSouth: {}, + RegionBrazilSoutheast: {}, + RegionCanadaCentral: {}, + RegionCanadaEast: {}, + RegionCentralIndia: {}, + RegionCentralUS: {}, + RegionChinaEast: {}, + RegionChinaEast2: {}, + RegionChinaNorth: {}, + RegionChinaNorth2: {}, + RegionEastAsia: {}, + RegionEastUS: {}, + RegionEastUS2: {}, + RegionFranceCentral: {}, + RegionFranceSouth: {}, + RegionGermanyNorth: {}, + RegionGermanyWestCentral: {}, + RegionIsraelCentral: {}, + RegionItalyNorth: {}, + RegionJapanEast: {}, + RegionJapanWest: {}, + RegionJioIndiaCentral: {}, + RegionJioIndiaWest: {}, + RegionKoreaCentral: {}, + RegionKoreaSouth: {}, + RegionMexicoCentral: {}, + RegionNorthCentralUS: {}, + RegionNorthEurope: {}, + RegionNorwayEast: {}, + RegionNorwayWest: {}, + RegionPolandCentral: {}, + RegionQatarCentral: {}, + RegionSouthAfricaNorth: {}, + RegionSouthAfricaWest: {}, + RegionSouthCentralUS: {}, + RegionSoutheastAsia: {}, + RegionSouthIndia: {}, + RegionSwedenCentral: {}, + RegionSwitzerlandNorth: {}, + RegionSwitzerlandWest: {}, + RegionUAECentral: {}, + RegionUAENorth: {}, + RegionUKSouth: {}, + RegionUKWest: {}, + RegionUSDoDCentral: {}, + RegionUSDoDEast: {}, + RegionUSGovArizona: {}, + RegionUSGovTexas: {}, + RegionUSGovVirginia: {}, + RegionWestCentralUS: {}, + RegionWestEurope: {}, + RegionWestIndia: {}, + RegionWestUS: {}, + RegionWestUS2: {}, + RegionWestUS3: {}, +} + +// Deprecated: use RegionFromName or RegionFromCommonEnum. +func ParseRegion(value string) (Region, error) { + // Polaris region name. + region := RegionFromCloudAccountRegionEnum(value) + if _, ok := validRegions[region]; ok { + return region, nil + } + + // Azure region name. + region = RegionFromName(value) + if _, ok := validRegions[region]; ok { + return region, nil + } + + return RegionUnknown, errors.New("invalid azure region") +} + +// Deprecated: no replacement. +func ParseRegions(values []string) ([]Region, error) { + regions := make([]Region, 0, len(values)) + + for _, r := range values { + region, err := ParseRegion(r) + if err != nil { + return nil, fmt.Errorf("failed to parse region: %v", err) + } + + regions = append(regions, region) + } + + return regions, nil +} + +// Deprecated: use RegionFromName. +func ParseRegionNoValidation(value string) Region { + return RegionFromName(value) +} + +// Deprecated: no replacement. +func ParseRegionsNoValidation(values []string) []Region { + regions := make([]Region, 0, len(values)) + for _, value := range values { + regions = append(regions, RegionFromName(value)) + } + + return regions +} diff --git a/pkg/polaris/graphql/azure/azure_test.go b/pkg/polaris/graphql/azure/regions_test.go similarity index 84% rename from pkg/polaris/graphql/azure/azure_test.go rename to pkg/polaris/graphql/azure/regions_test.go index 7bc42dfa..eca49a52 100644 --- a/pkg/polaris/graphql/azure/azure_test.go +++ b/pkg/polaris/graphql/azure/regions_test.go @@ -38,24 +38,12 @@ func TestFormatRegion(t *testing.T) { } func TestParseRegion(t *testing.T) { - region, err := ParseRegion("northeurope") - if err != nil { - t.Error(err) - } - if region != RegionNorthEurope { + if region := ParseRegionNoValidation("northeurope"); region != RegionNorthEurope { t.Errorf("invalid region: %v", region) } - regions, err := ParseRegions([]string{"eastus", "westus"}) - if err != nil { - t.Error(err) - } + regions := ParseRegionsNoValidation([]string{"eastus", "westus"}) if !reflect.DeepEqual(regions, []Region{RegionEastUS, RegionWestUS}) { t.Errorf("invalid region: %v", regions) } - - _, err = ParseRegion("space-moon-1") - if err == nil { - t.Error("expected test to fail") - } } diff --git a/pkg/polaris/graphql/core/core.go b/pkg/polaris/graphql/core/core.go index 80023f43..5a7dc7e6 100644 --- a/pkg/polaris/graphql/core/core.go +++ b/pkg/polaris/graphql/core/core.go @@ -20,8 +20,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package core provides a low level interface to core GraphQL queries provided -// by the Polaris platform. E.g. task chains and enum definitions. +// Package core provides a low-level interface to core GraphQL queries provided +// by the Polaris platform. E.g., task chains and enum definitions. package core import ( @@ -38,6 +38,15 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) +type CloudVendor string + +const ( + CloudVendorAWS CloudVendor = "AWS" + CloudVendorAzure CloudVendor = "AZURE" + CloudVendorGCP CloudVendor = "GCP" + CloudVendorAll CloudVendor = "ALL_VENDORS" +) + // CloudAccountAction represents a Polaris cloud account action. type CloudAccountAction string @@ -141,7 +150,7 @@ var ( FeatureArchival = Feature{Name: "ARCHIVAL"} FeatureAzureSQLDBProtection = Feature{Name: "AZURE_SQL_DB_PROTECTION"} FeatureAzureSQLMIProtection = Feature{Name: "AZURE_SQL_MI_PROTECTION"} - FeatureCloudAccounts = Feature{Name: "CLOUDACCOUNTS"} // Deprecated, no replacement. + FeatureCloudAccounts = Feature{Name: "CLOUDACCOUNTS"} // Deprecated: no replacement. FeatureCloudNativeArchival = Feature{Name: "CLOUD_NATIVE_ARCHIVAL"} FeatureCloudNativeArchivalEncryption = Feature{Name: "CLOUD_NATIVE_ARCHIVAL_ENCRYPTION"} FeatureCloudNativeBLOBProtection = Feature{Name: "CLOUD_NATIVE_BLOB_PROTECTION"} @@ -149,9 +158,9 @@ var ( FeatureCloudNativeS3Protection = Feature{Name: "CLOUD_NATIVE_S3_PROTECTION"} FeatureExocompute = Feature{Name: "EXOCOMPUTE"} FeatureGCPSharedVPCHost = Feature{Name: "GCP_SHARED_VPC_HOST"} - FeatureServerAndApps = Feature{Name: "SERVERS_AND_APPS"} - FeatureRDSProtection = Feature{Name: "RDS_PROTECTION"} FeatureKubernetesProtection = Feature{Name: "KUBERNETES_PROTECTION"} + FeatureRDSProtection = Feature{Name: "RDS_PROTECTION"} + FeatureServerAndApps = Feature{Name: "SERVERS_AND_APPS"} ) var validFeatures = map[string]struct{}{ @@ -160,7 +169,7 @@ var validFeatures = map[string]struct{}{ FeatureArchival.Name: {}, FeatureAzureSQLDBProtection.Name: {}, FeatureAzureSQLMIProtection.Name: {}, - FeatureCloudAccounts.Name: {}, + FeatureCloudAccounts.Name: {}, // Deprecated: no replacement. FeatureCloudNativeArchival.Name: {}, FeatureCloudNativeArchivalEncryption.Name: {}, FeatureCloudNativeBLOBProtection.Name: {}, @@ -185,29 +194,33 @@ func ContainsFeature(features []Feature, feature Feature) bool { return false } -// FormatFeature returns the Feature as a string using lower case and with -// hyphen as a separator. +// Deprecated: use Feature.Name instead. func FormatFeature(feature Feature) string { return strings.ReplaceAll(strings.ToLower(feature.Name), "_", "-") } -// ParseFeature returns the Feature matching the given feature name. -// Case-insensitive. +// Deprecated: use Feature{Name: } instead or ParseFeatureNoValidation +// if you need to remain backwards compatible with previously accepted feature +// names. func ParseFeature(feature string) (Feature, error) { - feature = strings.ReplaceAll(feature, "-", "_") - - name := strings.ToUpper(feature) - if _, ok := validFeatures[name]; ok { - return Feature{Name: name}, nil + f := ParseFeatureNoValidation(feature) + if _, ok := validFeatures[f.Name]; ok { + return f, nil } return FeatureInvalid, fmt.Errorf("invalid feature: %s", feature) } +// ParseFeatureNoValidation returns the Feature matching the given feature name. +// No validation is performed. +func ParseFeatureNoValidation(feature string) Feature { + return Feature{Name: strings.ToUpper(strings.ReplaceAll(feature, "-", "_"))} +} + const ( - // Number of attempts before failing to wait for the Korg job when the error - // returned is a 403, objects not authorized. - waitAttempts = 20 + // The number of attempts before failing to wait for the Korg job when the + // error returned is a 403, objects not authorized. + waitAttempts = 50 ) // Status represents a Polaris cloud account status. @@ -221,7 +234,7 @@ const ( StatusMissingPermissions Status = "MISSING_PERMISSIONS" ) -// FormatStatus returns the Status as a string using lower case and with hyphen +// FormatStatus returns the Status as a string using lower-case and with hyphen // as a separator. func FormatStatus(status Status) string { return strings.ReplaceAll(strings.ToLower(string(status)), "_", "-") @@ -277,19 +290,19 @@ type TaskChain struct { } // KorgTaskChainStatus returns the task chain for the specified task chain id. -// If the task chain id refers to a task chain that was just created its state +// If the task chain id refers to a task chain that was just created, its state // might not have reached ready yet. This can be detected by state being // TaskChainInvalid and error is nil. -func (a API) KorgTaskChainStatus(ctx context.Context, id uuid.UUID) (TaskChain, error) { +func (a API) KorgTaskChainStatus(ctx context.Context, taskChainID uuid.UUID) (TaskChain, error) { a.log.Print(log.Trace) buf, err := a.GQL.Request(ctx, getKorgTaskchainStatusQuery, struct { TaskChainID uuid.UUID `json:"taskchainId,omitempty"` - }{TaskChainID: id}) + }{TaskChainID: taskChainID}) if err != nil { return TaskChain{}, fmt.Errorf("failed to request getKorgTaskchainStatus: %w", err) } - a.log.Printf(log.Debug, "getKorgTaskchainStatus(%q): %s", id, string(buf)) + a.log.Printf(log.Debug, "getKorgTaskchainStatus(%q): %s", taskChainID, string(buf)) var payload struct { Data struct { @@ -299,29 +312,29 @@ func (a API) KorgTaskChainStatus(ctx context.Context, id uuid.UUID) (TaskChain, } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return TaskChain{}, fmt.Errorf("failed to unmarshal getKorgTaskchainStatus: %v", err) + return TaskChain{}, fmt.Errorf("failed to unmarshal getKorgTaskchainStatus: %s", err) } return payload.Data.Query.TaskChain, nil } // WaitForTaskChain blocks until the Polaris task chain with the specified task -// chain id has completed. When the task chain completes the final state of the +// chain id has completed. When the task chain completes, the final state of the // task chain is returned. The wait parameter specifies the amount of time to // wait before requesting another task status update. -func (a API) WaitForTaskChain(ctx context.Context, id uuid.UUID, wait time.Duration) (TaskChainState, error) { +func (a API) WaitForTaskChain(ctx context.Context, taskChainID uuid.UUID, wait time.Duration) (TaskChainState, error) { a.log.Print(log.Trace) attempt := 0 for { - taskChain, err := a.KorgTaskChainStatus(ctx, id) + taskChain, err := a.KorgTaskChainStatus(ctx, taskChainID) if err != nil { var gqlErr graphql.GQLError if !errors.As(err, &gqlErr) || len(gqlErr.Errors) < 1 || gqlErr.Errors[0].Extensions.Code != 403 { - return TaskChainInvalid, fmt.Errorf("failed to get tashchain status for %q: %v", id, err) + return TaskChainInvalid, fmt.Errorf("failed to get tashchain status for %s: %s", taskChainID, err) } if attempt++; attempt > waitAttempts { - return TaskChainInvalid, fmt.Errorf("failed to get tashchain status for %q after %d attempts: %v", id, attempt, err) + return TaskChainInvalid, fmt.Errorf("failed to get tashchain status for %s after %d attempts: %s", taskChainID, attempt, err) } a.log.Printf(log.Debug, "RBAC not ready (attempt: %d)", attempt) } @@ -330,7 +343,7 @@ func (a API) WaitForTaskChain(ctx context.Context, id uuid.UUID, wait time.Durat return taskChain.State, nil } - a.log.Printf(log.Debug, "Waiting for Polaris task chain: %v", id) + a.log.Printf(log.Debug, "Waiting for Polaris task chain: %s", taskChainID) select { case <-time.After(wait): @@ -340,6 +353,49 @@ func (a API) WaitForTaskChain(ctx context.Context, id uuid.UUID, wait time.Durat } } +// WaitForFeatureDisableTaskChain waits for the feature disable task chain to +// finish. If an error occurs while waiting for the task chain or the task chain +// ends in a failed state, an error is returned. +func (a API) WaitForFeatureDisableTaskChain(ctx context.Context, taskChainID uuid.UUID, featureStatus func(ctx context.Context) (bool, error)) error { + a.log.Print(log.Trace) + + ctx, cancel := context.WithTimeout(ctx, 9*time.Minute) + defer cancel() + for { + // Check the status of the task chain. + taskChain, err := a.KorgTaskChainStatus(ctx, taskChainID) + if err != nil { + // If the error isn't a 403, objects not authorized, we abort the wait. + var gqlErr graphql.GQLError + if !errors.As(err, &gqlErr) || len(gqlErr.Errors) < 1 || gqlErr.Errors[0].Extensions.Code != 403 { + return fmt.Errorf("failed to retrieve taskchain status: %s", err) + } + + // If the task chain RBAC is not yet ready, we fall back to checking + // the status of the account feature. + if disabled, err := featureStatus(ctx); disabled || err != nil { + return err + } + + a.log.Printf(log.Debug, "Task chain RBAC not ready") + } else { + if taskChain.State == TaskChainSucceeded { + return nil + } + if taskChain.State == TaskChainCanceled || taskChain.State == TaskChainFailed { + return fmt.Errorf("taskchain failed: task chain state is %s", taskChain.State) + } + } + + a.log.Printf(log.Debug, "Waiting for task chain: %s", taskChainID) + select { + case <-time.After(10 * time.Second): + case <-ctx.Done(): + return ctx.Err() + } + } +} + // Deprecated: use GQL.DeploymentVersion. func (a API) DeploymentVersion(ctx context.Context) (string, error) { a.log.Print(log.Trace) @@ -396,7 +452,7 @@ func (a API) EnabledFeaturesForAccount(ctx context.Context) ([]Feature, error) { var payload struct { Data struct { Result struct { - Features []Feature `json:"features"` + Features []string `json:"features"` } `json:"result"` } `json:"data"` } @@ -404,5 +460,10 @@ func (a API) EnabledFeaturesForAccount(ctx context.Context) ([]Feature, error) { return nil, fmt.Errorf("failed to unmarshal allEnabledFeaturesForAccount: %v", err) } - return payload.Data.Result.Features, nil + var features []Feature + for _, feature := range payload.Data.Result.Features { + features = append(features, Feature{Name: feature}) + } + + return features, nil } diff --git a/pkg/polaris/graphql/core/core_test.go b/pkg/polaris/graphql/core/core_test.go index 1162f8e4..7ef6af39 100644 --- a/pkg/polaris/graphql/core/core_test.go +++ b/pkg/polaris/graphql/core/core_test.go @@ -36,42 +36,22 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -func TestParseFeature(t *testing.T) { - feature, err := ParseFeature("CLOUD_NATIVE_PROTECTION") - if err != nil { - t.Error(err) - } - if !feature.Equal(FeatureCloudNativeProtection) { - t.Errorf("invalid feature: %s", feature) - } - - feature, err = ParseFeature("cloud_native_protection") - if err != nil { - t.Error(err) - } - if !feature.Equal(FeatureCloudNativeProtection) { +func TestParseFeatureNoValidation(t *testing.T) { + if feature := ParseFeatureNoValidation("CLOUD_NATIVE_PROTECTION"); !feature.Equal(FeatureCloudNativeProtection) { t.Errorf("invalid feature: %s", feature) } - feature, err = ParseFeature("cloud-native-protection") - if err != nil { - t.Error(err) - } - if !feature.Equal(FeatureCloudNativeProtection) { + if feature := ParseFeatureNoValidation("cloud_native_protection"); !feature.Equal(FeatureCloudNativeProtection) { t.Errorf("invalid feature: %s", feature) } - feature, err = ParseFeature("invalid-feature") - if err == nil { - t.Error("expected test to fail") - } - if !feature.Equal(FeatureInvalid) { + if feature := ParseFeatureNoValidation("cloud-native-protection"); !feature.Equal(FeatureCloudNativeProtection) { t.Errorf("invalid feature: %s", feature) } } func TestKorgTaskChainStatus(t *testing.T) { - tmpl, err := template.ParseFiles("testdata/korgtaskchainstatus.json") + tmpl, err := template.ParseFiles("testdata/korg_taskchain_status_response.json") if err != nil { t.Fatal(err) } @@ -124,7 +104,7 @@ func TestKorgTaskChainStatus(t *testing.T) { } func TestWaitForTaskChain(t *testing.T) { - tmpl, err := template.ParseFiles("testdata/korgtaskchainstatus.json") + tmpl, err := template.ParseFiles("testdata/korg_taskchain_status_response.json") if err != nil { t.Fatal(err) } @@ -132,8 +112,8 @@ func TestWaitForTaskChain(t *testing.T) { client, lis := graphql.NewTestClient("john", "doe", log.DiscardLogger{}) coreAPI := Wrap(client) - // Respond with status code 200 and a valid body. First 2 reponses have - // state RUNNING. Third response is SUCCEEDED. + // Respond with status code 200 and a valid body. The First 2 responses have + // state RUNNING. The Third response is SUCCEEDED. reqCount := 3 srv := testnet.ServeJSONWithStaticToken(lis, func(w http.ResponseWriter, req *http.Request) { buf, err := io.ReadAll(req.Body) diff --git a/pkg/polaris/graphql/core/testdata/korgtaskchainstatus.json b/pkg/polaris/graphql/core/testdata/korg_taskchain_status_response.json similarity index 100% rename from pkg/polaris/graphql/core/testdata/korgtaskchainstatus.json rename to pkg/polaris/graphql/core/testdata/korg_taskchain_status_response.json diff --git a/pkg/polaris/graphql/errors_test.go b/pkg/polaris/graphql/errors_test.go index be2a1b50..02c69e38 100644 --- a/pkg/polaris/graphql/errors_test.go +++ b/pkg/polaris/graphql/errors_test.go @@ -43,7 +43,7 @@ func TestErrorsWithNoError(t *testing.T) { } func TestGqlError(t *testing.T) { - buf, err := os.ReadFile("testdata/error_graphql.json") + buf, err := os.ReadFile("testdata/graphql_error_response.json") if err != nil { t.Fatal(err) } diff --git a/pkg/polaris/graphql/exocompute/exocompute.go b/pkg/polaris/graphql/exocompute/exocompute.go new file mode 100644 index 00000000..7f982438 --- /dev/null +++ b/pkg/polaris/graphql/exocompute/exocompute.go @@ -0,0 +1,284 @@ +//go:generate go run ../queries_gen.go exocompute + +// Copyright 2024 Rubrik, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +package exocompute + +import ( + "context" + "encoding/json" + "errors" + + "github.com/google/uuid" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/azure" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" +) + +// ListResult represents the result of a list operation. +type ListResult interface { + ListQuery(filter string) (string, any) +} + +// ListConfigurations return all exocompute configurations matching the +// specified filter. +func ListConfigurations[Result ListResult](ctx context.Context, gql *graphql.Client, filter string) ([]Result, error) { + gql.Log().Print(log.Trace) + + var result Result + query, params := result.ListQuery(filter) + buf, err := gql.Request(ctx, query, params) + if err != nil { + return nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result []Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, graphql.UnmarshalError(query, err) + } + + return payload.Data.Result, nil +} + +// CreateParams represents the valid type parameters for a create operation. +type CreateParams interface { + aws.ExoCreateParams | azure.ExoCreateParams +} + +// CreateResult represents the result of a create operation. +type CreateResult[Params CreateParams] interface { + CreateQuery(cloudAccountID uuid.UUID, createParams Params) (string, any) + Validate() (uuid.UUID, error) +} + +// CreateConfiguration creates a new exocompute configuration in the account +// with the specified RSC cloud account id. Returns the ID of the configuration. +func CreateConfiguration[Result CreateResult[Params], Params CreateParams](ctx context.Context, gql *graphql.Client, cloudAccountID uuid.UUID, createParams Params) (uuid.UUID, error) { + gql.Log().Print(log.Trace) + + var result Result + query, queryParams := result.CreateQuery(cloudAccountID, createParams) + buf, err := gql.Request(ctx, query, queryParams) + if err != nil { + return uuid.Nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return uuid.Nil, graphql.UnmarshalError(query, err) + } + id, err := payload.Data.Result.Validate() + if err != nil { + return uuid.Nil, graphql.ResponseError(query, err) + } + + return id, nil +} + +// UpdateParams represents the valid type parameters for an update operation. +type UpdateParams interface { + aws.ExoUpdateParams +} + +// UpdateResult represents the result of an update operation. +type UpdateResult[Params UpdateParams] interface { + UpdateQuery(cloudAccountID uuid.UUID, updateParams Params) (string, any) + Validate() (uuid.UUID, error) +} + +// UpdateConfiguration updates an existing exocompute configuration in the +// account with the specified RSC cloud account id. +func UpdateConfiguration[Result UpdateResult[Params], Params UpdateParams](ctx context.Context, gql *graphql.Client, cloudAccountID uuid.UUID, updateParams Params) (uuid.UUID, error) { + gql.Log().Print(log.Trace) + + var result Result + query, queryParams := result.UpdateQuery(cloudAccountID, updateParams) + buf, err := gql.Request(ctx, query, queryParams) + if err != nil { + return uuid.Nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return uuid.Nil, graphql.UnmarshalError(query, err) + } + id, err := payload.Data.Result.Validate() + if err != nil { + return uuid.Nil, graphql.ResponseError(query, err) + } + + return id, nil +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult interface { + DeleteQuery(configID uuid.UUID) (string, any) + Validate() (uuid.UUID, error) +} + +// DeleteConfiguration deletes the exocompute configuration with the specified +// configuration ID. +func DeleteConfiguration[Result DeleteResult](ctx context.Context, gql *graphql.Client, configID uuid.UUID) error { + gql.Log().Print(log.Trace) + + var result Result + query, params := result.DeleteQuery(configID) + buf, err := gql.Request(ctx, query, params) + if err != nil { + return graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return graphql.UnmarshalError(query, err) + } + id, err := payload.Data.Result.Validate() + if err != nil { + return graphql.ResponseError(query, err) + } + if id != configID { + return graphql.ResponseError(query, errors.New("response ID does not match request ID")) + } + + return nil +} + +// MapResult represents the result of a map operation. +type MapResult interface { + MapQuery(hostCloudAccountID, appCloudAccountIDs uuid.UUID) (string, any) + Validate() error +} + +// MapCloudAccount maps the application cloud account to the host cloud account. +func MapCloudAccount[Result MapResult](ctx context.Context, gql *graphql.Client, hostCloudAccountID, appCloudAccountID uuid.UUID) error { + gql.Log().Print(log.Trace) + + var result Result + query, params := result.MapQuery(hostCloudAccountID, appCloudAccountID) + buf, err := gql.Request(ctx, query, params) + if err != nil { + return graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return graphql.UnmarshalError(query, err) + } + if err := payload.Data.Result.Validate(); err != nil { + return graphql.ResponseError(query, err) + } + + return nil +} + +// UnmapResult represents the result of an unmap operation. +type UnmapResult interface { + UnmapQuery(appCloudAccountIDs uuid.UUID) (string, any) + Validate() error +} + +// UnmapCloudAccount unmaps the application cloud account. +func UnmapCloudAccount[Result UnmapResult](ctx context.Context, gql *graphql.Client, appCloudAccountID uuid.UUID) error { + gql.Log().Print(log.Trace) + + var result Result + query, params := result.UnmapQuery(appCloudAccountID) + buf, err := gql.Request(ctx, query, params) + if err != nil { + return graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result Result `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return graphql.UnmarshalError(query, err) + } + if err := payload.Data.Result.Validate(); err != nil { + return graphql.ResponseError(query, err) + } + + return nil +} + +// CloudAccountMapping represents a mapping between an exocompute application +// cloud account and a host cloud account. +type CloudAccountMapping struct { + AppCloudAccountID uuid.UUID `json:"applicationCloudAccountId"` + HostCloudAccountID uuid.UUID `json:"exocomputeCloudAccountId"` +} + +// AllCloudAccountMappings returns all exocompute cloud account mappings for +// the specified cloud vendor. Note that only AWS and Azure are supported by +// RSC. +func AllCloudAccountMappings(ctx context.Context, gql *graphql.Client, cloudVendor core.CloudVendor) ([]CloudAccountMapping, error) { + gql.Log().Print(log.Trace) + + query := allCloudAccountExocomputeMappingsQuery + buf, err := gql.Request(ctx, query, struct { + CloudVendor core.CloudVendor `json:"cloudVendor"` + }{CloudVendor: cloudVendor}) + if err != nil { + return nil, graphql.RequestError(query, err) + } + graphql.LogResponse(gql.Log(), query, buf) + + var payload struct { + Data struct { + Result []CloudAccountMapping `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, graphql.UnmarshalError(query, err) + } + + return payload.Data.Result, nil +} diff --git a/pkg/polaris/graphql/exocompute/queries.go b/pkg/polaris/graphql/exocompute/queries.go new file mode 100644 index 00000000..319abb15 --- /dev/null +++ b/pkg/polaris/graphql/exocompute/queries.go @@ -0,0 +1,54 @@ +// Code generated by queries_gen.go DO NOT EDIT. + +// MIT License +// +// Copyright (c) 2021 Rubrik +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package exocompute + +// allCloudAccountExocomputeMappings GraphQL query +var allCloudAccountExocomputeMappingsQuery = `query SdkGolangAllCloudAccountExocomputeMappings($cloudVendor: CloudVendor!) { + result: allCloudAccountExocomputeMappings(cloudVendor: $cloudVendor) { + applicationCloudAccountId + exocomputeCloudAccountId + } +}` + +// mapCloudAccountExocomputeAccount GraphQL query +var mapCloudAccountExocomputeAccountQuery = `mutation SdkGolangMapCloudAccountExocomputeAccount($cloudVendor: CloudVendor!, $exocomputeCloudAccountId: UUID!, $cloudAccountIds: [UUID!]!) { + result: mapCloudAccountExocomputeAccount(input: { + exocomputeCloudAccountId: $exocomputeCloudAccountId, + cloudAccountIds: $cloudAccountIds, + cloudVendor: $cloudVendor + }) { + isSuccess + } +}` + +// unmapCloudAccountExocomputeAccount GraphQL query +var unmapCloudAccountExocomputeAccountQuery = `mutation SdkGolangUnmapCloudAccountExocomputeAccount($cloudVendor: CloudVendor!, $cloudAccountIds: [UUID!]!) { + result: unmapCloudAccountExocomputeAccount(input: { + cloudAccountIds: $cloudAccountIds, + cloudVendor: $cloudVendor, + }) { + isSuccess + } +}` diff --git a/pkg/polaris/graphql/exocompute/queries/all_cloud_account_exocompute_mappings.graphql b/pkg/polaris/graphql/exocompute/queries/all_cloud_account_exocompute_mappings.graphql new file mode 100644 index 00000000..daa8f8cd --- /dev/null +++ b/pkg/polaris/graphql/exocompute/queries/all_cloud_account_exocompute_mappings.graphql @@ -0,0 +1,6 @@ +query RubrikPolarisSDKRequest($cloudVendor: CloudVendor!) { + result: allCloudAccountExocomputeMappings(cloudVendor: $cloudVendor) { + applicationCloudAccountId + exocomputeCloudAccountId + } +} diff --git a/pkg/polaris/graphql/exocompute/queries/map_cloud_account_exocompute_account.graphql b/pkg/polaris/graphql/exocompute/queries/map_cloud_account_exocompute_account.graphql new file mode 100644 index 00000000..f82000df --- /dev/null +++ b/pkg/polaris/graphql/exocompute/queries/map_cloud_account_exocompute_account.graphql @@ -0,0 +1,9 @@ +mutation RubrikPolarisSDKRequest($cloudVendor: CloudVendor!, $exocomputeCloudAccountId: UUID!, $cloudAccountIds: [UUID!]!) { + result: mapCloudAccountExocomputeAccount(input: { + exocomputeCloudAccountId: $exocomputeCloudAccountId, + cloudAccountIds: $cloudAccountIds, + cloudVendor: $cloudVendor + }) { + isSuccess + } +} diff --git a/pkg/polaris/graphql/exocompute/queries/unmap_cloud_account_exocompute_account.graphql b/pkg/polaris/graphql/exocompute/queries/unmap_cloud_account_exocompute_account.graphql new file mode 100644 index 00000000..10688d84 --- /dev/null +++ b/pkg/polaris/graphql/exocompute/queries/unmap_cloud_account_exocompute_account.graphql @@ -0,0 +1,8 @@ +mutation RubrikPolarisSDKRequest($cloudVendor: CloudVendor!, $cloudAccountIds: [UUID!]!) { + result: unmapCloudAccountExocomputeAccount(input: { + cloudAccountIds: $cloudAccountIds, + cloudVendor: $cloudVendor, + }) { + isSuccess + } +} diff --git a/pkg/polaris/graphql/gcp/cloud.go b/pkg/polaris/graphql/gcp/cloud.go index 8143b6b3..1160cd3a 100644 --- a/pkg/polaris/graphql/gcp/cloud.go +++ b/pkg/polaris/graphql/gcp/cloud.go @@ -88,7 +88,7 @@ func (a API) CloudAccountProjectsByFeature(ctx context.Context, feature core.Fea func (a API) CloudAccountAddManualAuthProject(ctx context.Context, projectID, projectName string, projectNumber int64, orgName, jwtConfig string, feature core.Feature) error { a.log.Print(log.Trace) - _, err := a.GQL.Request(ctx, gcpCloudAccountAddManualAuthProjectQuery, struct { + _, err := a.GQL.RequestWithoutLogging(ctx, gcpCloudAccountAddManualAuthProjectQuery, struct { ID string `json:"gcpNativeProjectId"` Name string `json:"gcpProjectName"` Number int64 `json:"gcpProjectNumber"` diff --git a/pkg/polaris/graphql/gcp/gcp.go b/pkg/polaris/graphql/gcp/gcp.go index 24d1eafc..7a965510 100644 --- a/pkg/polaris/graphql/gcp/gcp.go +++ b/pkg/polaris/graphql/gcp/gcp.go @@ -75,7 +75,7 @@ func (a API) DefaultCredentialsServiceAccount(ctx context.Context) (name string, func (a API) SetDefaultServiceAccount(ctx context.Context, name, jwtConfig string) error { a.log.Print(log.Trace) - buf, err := a.GQL.Request(ctx, gcpSetDefaultServiceAccountJwtConfigQuery, struct { + buf, err := a.GQL.RequestWithoutLogging(ctx, gcpSetDefaultServiceAccountJwtConfigQuery, struct { Name string `json:"serviceAccountName"` JwtConfig string `json:"serviceAccountJwtConfig"` }{Name: name, JwtConfig: jwtConfig}) diff --git a/pkg/polaris/graphql/graphql.go b/pkg/polaris/graphql/graphql.go index 3b82e2ee..f60a14b4 100644 --- a/pkg/polaris/graphql/graphql.go +++ b/pkg/polaris/graphql/graphql.go @@ -97,7 +97,7 @@ func NewClientFromServiceAccount(app, apiURL, accessTokenURI, clientID, clientSe // NewTestClient returns a new Client intended to be used by unit tests. func NewTestClient(username, password string, logger log.Logger) (*Client, *testnet.TestListener) { testClient, listener := testnet.NewPipeNet() - tokenSource := token.NewUserSourceWithLogger(testClient, "http://test/api", username, password, logger) + tokenSource := token.NewUserSourceWithLogger(testClient, "http://test/api/session", username, password, logger) client := &Client{ gqlURL: "http://test/api/graphql", @@ -144,12 +144,29 @@ func (c *Client) SetLogger(logger log.Logger) { const requestRetryAttempts = 10 -// Request posts the specified GraphQL query with the given variables to the -// Polaris platform. Returns the response JSON text as is. If the request fails -// due to a temporary error, it will be retried automatically. +// Request posts the specified GraphQL query/mutation with the given variables +// to the Polaris platform. Returns the response JSON text as is. If the request +// fails due to temporary error, it will be retried automatically. func (c *Client) Request(ctx context.Context, query string, variables interface{}) ([]byte, error) { c.log.Print(log.Trace) + // Log variables before calling the query/mutation. + buf, err := json.Marshal(variables) + if err != nil { + buf = []byte(fmt.Sprintf("marshaling of variables failed: %s", err)) + } + c.log.Printf(log.Debug, "%s params: %s", QueryName(query), string(buf)) + + return c.RequestWithoutLogging(ctx, query, variables) +} + +// RequestWithoutLogging posts the specified GraphQL query/mutation with the +// given variables to the Polaris platform. Returns the response JSON text as +// is. The variables are not logged before the request is made. Certain +// temporary errors will be retried. +func (c *Client) RequestWithoutLogging(ctx context.Context, query string, variables interface{}) ([]byte, error) { + c.log.Print(log.Trace) + retryAttempt := 0 for { buf, err := c.RequestWithoutRetry(ctx, query, variables) @@ -174,7 +191,7 @@ func (c *Client) Request(ctx context.Context, query string, variables interface{ } } -// RequestWithoutRetry posts the specified GraphQL query with the given +// RequestWithoutRetry posts the specified GraphQL query/mutation with the given // variables to the Polaris platform. Returns the response JSON text as is. func (c *Client) RequestWithoutRetry(ctx context.Context, query string, variables interface{}) ([]byte, error) { c.log.Print(log.Trace) @@ -258,6 +275,29 @@ func (c *Client) RequestWithoutRetry(ctx context.Context, query string, variable return buf, nil } +// LogResponse logs the response from a GraphQL query/mutation. +func LogResponse(logger log.Logger, query string, response []byte) { + logger.Printf(log.Debug, "%s response: %s", query, string(response)) +} + +// RequestError returns a standard formatted error detailing the failure when +// calling a query/mutation. +func RequestError(query string, err error) error { + return fmt.Errorf("failed to request %s: %w", QueryName(query), err) +} + +// UnmarshalError returns a standard formatted error detailing the failure to +// unmarshal the response from a GraphQL query/mutation. +func UnmarshalError(query string, err error) error { + return fmt.Errorf("failed to unmarshal %s: %s", QueryName(query), err) +} + +// ResponseError returns a standard formatted error detailing the error received +// in response to a query/mutation. +func ResponseError(query string, err error) error { + return fmt.Errorf("%s response is an error: %s", QueryName(query), err) +} + // operationName tries to extract the operation name from a query // e.g.: // diff --git a/pkg/polaris/graphql/graphql_test.go b/pkg/polaris/graphql/graphql_test.go index 9100f0c9..b3f2e759 100644 --- a/pkg/polaris/graphql/graphql_test.go +++ b/pkg/polaris/graphql/graphql_test.go @@ -32,7 +32,7 @@ import ( ) func TestRequestUnauthenticated(t *testing.T) { - tmpl, err := template.ParseFiles("testdata/error_json_from_auth.json") + tmpl, err := template.ParseFiles("testdata/auth_error_response.json") if err != nil { t.Fatal(err) } @@ -58,7 +58,7 @@ func TestRequestUnauthenticated(t *testing.T) { } func TestRequestWithInternalServerErrorJSONBody(t *testing.T) { - tmpl, err := template.ParseFiles("testdata/error_graphql.json") + tmpl, err := template.ParseFiles("testdata/graphql_error_response.json") if err != nil { t.Fatal(err) } diff --git a/pkg/polaris/graphql/testdata/error_json_from_auth.json b/pkg/polaris/graphql/testdata/auth_error_response.json similarity index 100% rename from pkg/polaris/graphql/testdata/error_json_from_auth.json rename to pkg/polaris/graphql/testdata/auth_error_response.json diff --git a/pkg/polaris/graphql/testdata/error_graphql.json b/pkg/polaris/graphql/testdata/graphql_error_response.json similarity index 100% rename from pkg/polaris/graphql/testdata/error_graphql.json rename to pkg/polaris/graphql/testdata/graphql_error_response.json diff --git a/pkg/polaris/polaris.go b/pkg/polaris/polaris.go index 87e8c6e6..c573919b 100644 --- a/pkg/polaris/polaris.go +++ b/pkg/polaris/polaris.go @@ -19,14 +19,13 @@ // DEALINGS IN THE SOFTWARE. // Package polaris contains code to interact with the RSC platform on a high -// level. Relies on the graphql package for low level queries. +// level. Relies on the graphql package for low-level queries. package polaris import ( "errors" "fmt" "net/http" - "net/url" "os" "strconv" "strings" @@ -44,21 +43,38 @@ const ( DefaultServiceAccountFile = "~/.rubrik/polaris-service-account.json" ) -// Client is used to make calls to the RSC platform. -type Client struct { - GQL *graphql.Client -} - // Account represents a Polaris account. Implemented by UserAccount and // ServiceAccount. type Account interface { + // AccountName returns the RSC account name. + AccountName() string + + // AccountFQDN returns the fully qualified domain name of the RSC account. + AccountFQDN() string + + // APIURL returns the RSC account API URL. + APIURL() string + + // TokenURL returns the RSC account token URL. + TokenURL() string + allowEnvOverride() bool + + // Cryptographic material for encrypting cached access tokens. + cacheKeyMaterial() string + cacheSuffixMaterial() string +} + +// Client is used to make calls to the RSC platform. +type Client struct { + Account Account + GQL *graphql.Client } // NewClient returns a new Client for the specified Account. // // The client will cache authentication tokens by default, this behavior can be -// overriden by setting the environment variable RUBRIK_POLARIS_TOKEN_CACHE to +// overridden by setting the environment variable RUBRIK_POLARIS_TOKEN_CACHE to // false, given that the account specified allows environment variable // overrides. func NewClient(account Account) (*Client, error) { @@ -68,7 +84,7 @@ func NewClient(account Account) (*Client, error) { // NewClientWithLogger returns a new Client for the specified Account. // // The client will cache authentication tokens by default, this behavior can be -// overriden by setting the environment variable RUBRIK_POLARIS_TOKEN_CACHE to +// overridden by setting the environment variable RUBRIK_POLARIS_TOKEN_CACHE to // false, given that the account specified allows environment variable // overrides. func NewClientWithLogger(account Account, logger log.Logger) (*Client, error) { @@ -81,21 +97,31 @@ func NewClientWithLogger(account Account, logger log.Logger) (*Client, error) { } } - var client *Client - var err error + var tokenSource token.Source switch account := account.(type) { case *UserAccount: - client, err = newClientFromUserAccount(account, logger, cacheToken) + tokenSource = token.NewUserSourceWithLogger( + http.DefaultClient, account.TokenURL(), account.Username, account.Password, logger) case *ServiceAccount: - client, err = newClientFromServiceAccount(account, logger, cacheToken) + tokenSource = token.NewServiceAccountSourceWithLogger( + http.DefaultClient, account.TokenURL(), account.ClientID, account.ClientSecret, logger) default: - err = errors.New("invalid account type") + return nil, errors.New("failed to create client: invalid account type") } - if err != nil { - return nil, fmt.Errorf("failed to create client: %s", err) + + if cacheToken { + var err error + tokenSource, err = token.NewCache( + tokenSource, account.cacheKeyMaterial(), account.cacheSuffixMaterial(), account.allowEnvOverride()) + if err != nil { + return nil, fmt.Errorf("failed to create token cache: %s", err) + } } - return client, nil + return &Client{ + Account: account, + GQL: graphql.NewClientWithLogger(account.APIURL(), tokenSource, logger), + }, nil } // SetLogger sets the logger to use. @@ -122,77 +148,3 @@ func SetLogLevelFromEnv(logger log.Logger) error { return nil } - -// newClientFromUserAccount returns a new Client from the specified UserAccount. -func newClientFromUserAccount(account *UserAccount, logger log.Logger, cacheToken bool) (*Client, error) { - apiURL := account.URL - if apiURL == "" { - apiURL = fmt.Sprintf("https://%s.my.rubrik.com/api", account.Name) - } - if _, err := url.ParseRequestURI(apiURL); err != nil { - return nil, fmt.Errorf("invalid url: %s", err) - } - if account.Username == "" { - return nil, errors.New("invalid username") - } - if account.Password == "" { - return nil, errors.New("invalid password") - } - - var tokenSource token.Source = token.NewUserSourceWithLogger(http.DefaultClient, apiURL, account.Username, account.Password, logger) - if cacheToken { - var err error - tokenSource, err = token.NewCache(tokenSource, - account.Name+account.URL+account.Username+account.Password, account.Name+account.Username, account.allowEnvOverride()) - if err != nil { - return nil, fmt.Errorf("failed to create token cache: %s", err) - } - } - - client := &Client{ - GQL: graphql.NewClientWithLogger(apiURL, tokenSource, logger), - } - - return client, nil -} - -// newClientFromServiceAccount returns a new Client from the specified -// ServiceAccount. -func newClientFromServiceAccount(account *ServiceAccount, logger log.Logger, cacheToken bool) (*Client, error) { - if account.Name == "" { - return nil, errors.New("invalid name") - } - if account.ClientID == "" { - return nil, errors.New("invalid client id") - } - if account.ClientSecret == "" { - return nil, errors.New("invalid client secret") - } - if _, err := url.ParseRequestURI(account.AccessTokenURI); err != nil { - return nil, fmt.Errorf("invalid access token uri: %s", err) - } - - // Extract the API URL from the token access URI. - i := strings.LastIndex(account.AccessTokenURI, "/") - if i < 0 { - return nil, errors.New("invalid access token uri") - } - apiURL := account.AccessTokenURI[:i] - - var tokenSource token.Source = token.NewServiceAccountSourceWithLogger( - http.DefaultClient, account.AccessTokenURI, account.ClientID, account.ClientSecret, logger) - if cacheToken { - var err error - tokenSource, err = token.NewCache(tokenSource, - account.Name+account.AccessTokenURI+account.ClientID+account.ClientSecret, account.Name+account.ClientID, account.allowEnvOverride()) - if err != nil { - return nil, fmt.Errorf("failed to create token cache: %s", err) - } - } - - client := &Client{ - GQL: graphql.NewClientWithLogger(apiURL, tokenSource, logger), - } - - return client, nil -} diff --git a/pkg/polaris/token/cache.go b/pkg/polaris/token/cache.go index 8fc3b8ce..115a0224 100644 --- a/pkg/polaris/token/cache.go +++ b/pkg/polaris/token/cache.go @@ -52,13 +52,13 @@ type cache struct { // NewCache returns a new cache wrapping the specified token source. // // The cache will store authentication tokens in the OS default directory for -// temporary files, this behavior can be overriden by setting the environment +// temporary files. This behavior can be overridden by setting the environment // variable RUBRIK_POLARIS_TOKEN_CACHE_DIR to the directory to use, given that // the account passed in when creating the client allows environment variable // overrides. // // The cache will also generate a key to encrypt the content of the token cache -// from the RSC account, this behavior can be overriden by setting the +// from the RSC account, this behavior can be overridden by setting the // environment variable RUBRIK_POLARIS_TOKEN_CACHE_SECRET to the secret used // when generating the encryption key, given that the account passed in when // creating the client allows environment variable overrides. diff --git a/pkg/polaris/token/cache_test.go b/pkg/polaris/token/cache_test.go index 97f4f0b0..0e624c30 100644 --- a/pkg/polaris/token/cache_test.go +++ b/pkg/polaris/token/cache_test.go @@ -58,7 +58,7 @@ func TestCacheTokenSource(t *testing.T) { t.Fatalf("wrong token: %s", tok) } - // The wrapped token source return the unexpired token. + // The wrapped token source returns the unexpired token. testToken, err = cache.token(context.Background()) if err != nil { t.Fatal(err) diff --git a/pkg/polaris/token/request.go b/pkg/polaris/token/request.go index ca8ba7b0..8cd616dc 100644 --- a/pkg/polaris/token/request.go +++ b/pkg/polaris/token/request.go @@ -66,8 +66,8 @@ func requestToken(ctx context.Context, client *http.Client, tokenURL string, req return nil, fmt.Errorf("failed to request token: %v", err) } defer res.Body.Close() - // Remote responded without a body. For status code 200 this means we are - // missing the token. For an error we have no additional details. + // Remote responded without a body. For status code 200, this means we are + // missing the token. For an error, we have no additional details. if res.ContentLength == 0 { return nil, fmt.Errorf("token response has no body (status code %d)", res.StatusCode) } @@ -80,8 +80,8 @@ func requestToken(ctx context.Context, client *http.Client, tokenURL string, req return nil, fmt.Errorf("failed to read token response body (status code %d): %v", res.StatusCode, err) } - // Verify that the content type of the body is JSON. For status code 200 - // this mean we received something that isn't JSON. For an error we have no + // Verify that the content type of the body is JSON. For status code 200, + // this means we received something that isn't JSON. For an error, we have no // additional JSON details. contentType := res.Header.Get("Content-Type") if !strings.HasPrefix(contentType, "application/json") { diff --git a/pkg/polaris/token/testdata/error_json_from_polaris.json b/pkg/polaris/token/testdata/auth_error_response.json similarity index 100% rename from pkg/polaris/token/testdata/error_json_from_polaris.json rename to pkg/polaris/token/testdata/auth_error_response.json diff --git a/pkg/polaris/token/token_test.go b/pkg/polaris/token/token_test.go index 068243fb..dfbdd14f 100644 --- a/pkg/polaris/token/token_test.go +++ b/pkg/polaris/token/token_test.go @@ -118,7 +118,7 @@ func TestTokenSource(t *testing.T) { func TestTokenSourceWithBadCredentials(t *testing.T) { ctx := context.Background() - tmpl, err := template.ParseFiles("testdata/error_json_from_polaris.json") + tmpl, err := template.ParseFiles("testdata/auth_error_response.json") if err != nil { t.Fatal(err) } diff --git a/pkg/polaris/token/user_source.go b/pkg/polaris/token/user_source.go index 592c4072..288d0661 100644 --- a/pkg/polaris/token/user_source.go +++ b/pkg/polaris/token/user_source.go @@ -51,11 +51,11 @@ func NewUserSource(client *http.Client, apiURL, username, password string) *User // NewUserSourceWithLogger returns a new token source that uses the specified // client to obtain tokens. -func NewUserSourceWithLogger(client *http.Client, apiURL, username, password string, logger log.Logger) *UserSource { +func NewUserSourceWithLogger(client *http.Client, tokenURL, username, password string, logger log.Logger) *UserSource { return &UserSource{ log: logger, client: client, - tokenURL: apiURL + "/session", + tokenURL: tokenURL, username: username, password: password, }