From 69e4f2f307d792aba5eccf9c71a55b03880dcecb Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Thu, 31 Aug 2023 10:10:39 +0200 Subject: [PATCH 1/6] Add support for AWS account onboarding without CloudFormation --- cmd/testenv/main.go | 4 +- examples/aws_account/main.go | 4 +- examples/aws_cross_account_role/main.go | 4 +- examples/aws_exocompute/main.go | 8 +- pkg/polaris/aws/account.go | 32 +- pkg/polaris/aws/aws.go | 408 ++++++++++++++---- pkg/polaris/aws/aws_test.go | 8 +- pkg/polaris/aws/exocompute_test.go | 8 +- pkg/polaris/aws/identity.go | 4 +- pkg/polaris/aws/permissions.go | 93 +++- pkg/polaris/graphql/aws/aws.go | 38 +- pkg/polaris/graphql/aws/cloud.go | 20 +- pkg/polaris/graphql/aws/cloud_no_cft.go | 199 +++++++++ pkg/polaris/graphql/aws/cloud_test.go | 2 +- pkg/polaris/graphql/aws/queries.go | 58 ++- .../all_aws_permission_policies.graphql | 17 + .../aws/queries/aws_trust_policy.graphql | 12 + ...lete_aws_cloud_account_without_cft.graphql | 8 + ...alize_aws_cloud_account_protection.graphql | 4 +- .../register_aws_feature_artifacts.graphql | 9 + pkg/polaris/graphql/core/core.go | 68 ++- pkg/polaris/graphql/core/queries.go | 7 + .../all_enabled_features_for_account.graphql | 5 + 23 files changed, 890 insertions(+), 130 deletions(-) create mode 100644 pkg/polaris/graphql/aws/cloud_no_cft.go create mode 100644 pkg/polaris/graphql/aws/queries/all_aws_permission_policies.graphql create mode 100644 pkg/polaris/graphql/aws/queries/aws_trust_policy.graphql create mode 100644 pkg/polaris/graphql/aws/queries/bulk_delete_aws_cloud_account_without_cft.graphql create mode 100644 pkg/polaris/graphql/aws/queries/register_aws_feature_artifacts.graphql create mode 100644 pkg/polaris/graphql/core/queries/all_enabled_features_for_account.graphql diff --git a/cmd/testenv/main.go b/cmd/testenv/main.go index 784ac845..187d8c1c 100644 --- a/cmd/testenv/main.go +++ b/cmd/testenv/main.go @@ -152,7 +152,7 @@ func clean(ctx context.Context, client *polaris.Client) error { // TODO: we might need to iterate over awsAccount.Features to remove // all of them in the future - return awsClient.RemoveAccount(ctx, aws.Profile(testAcc.Profile), core.FeatureCloudNativeProtection, false) + return awsClient.RemoveAccount(ctx, aws.Profile(testAcc.Profile), []core.Feature{core.FeatureCloudNativeProtection}, false) }) // AWS with cross account role @@ -177,7 +177,7 @@ func clean(ctx context.Context, client *polaris.Client) error { // TODO: we might need to iterate over awsAccount.Features to remove // all of them in the future - return awsClient.RemoveAccount(ctx, aws.DefaultWithRole(testAcc.CrossAccountRole), core.FeatureCloudNativeProtection, false) + return awsClient.RemoveAccount(ctx, aws.DefaultWithRole(testAcc.CrossAccountRole), []core.Feature{core.FeatureCloudNativeProtection}, false) }) // Azure diff --git a/examples/aws_account/main.go b/examples/aws_account/main.go index e7799bdf..54bee1b4 100644 --- a/examples/aws_account/main.go +++ b/examples/aws_account/main.go @@ -54,7 +54,7 @@ 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. - id, err := awsClient.AddAccount(ctx, aws.Default(), core.FeatureCloudNativeProtection, aws.Regions("us-east-2")) + id, err := awsClient.AddAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, aws.Regions("us-east-2")) if err != nil { log.Fatal(err) } @@ -71,7 +71,7 @@ func main() { } // Remove the AWS account from Polaris. - err = awsClient.RemoveAccount(ctx, aws.Default(), core.FeatureCloudNativeProtection, false) + err = awsClient.RemoveAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { log.Fatal(err) } diff --git a/examples/aws_cross_account_role/main.go b/examples/aws_cross_account_role/main.go index 7a7bf1dd..28fccbaa 100644 --- a/examples/aws_cross_account_role/main.go +++ b/examples/aws_cross_account_role/main.go @@ -57,7 +57,7 @@ func main() { // variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION. id, err := awsClient.AddAccount(ctx, aws.DefaultWithRole("arn:aws:iam::123456789012:role/MyCrossAccountRole"), - core.FeatureCloudNativeProtection, aws.Regions("us-east-2")) + []core.Feature{core.FeatureCloudNativeProtection}, aws.Regions("us-east-2")) if err != nil { log.Fatal(err) } @@ -76,7 +76,7 @@ func main() { // Remove the AWS account from Polaris using a cross account role. err = awsClient.RemoveAccount(ctx, aws.DefaultWithRole("arn:aws:iam::123456789012:role/MyCrossAccountRole"), - core.FeatureCloudNativeProtection, false) + []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { log.Fatal(err) } diff --git a/examples/aws_exocompute/main.go b/examples/aws_exocompute/main.go index 2f89aa09..f12de793 100644 --- a/examples/aws_exocompute/main.go +++ b/examples/aws_exocompute/main.go @@ -49,7 +49,7 @@ 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.FeatureCloudNativeProtection, aws.Regions("us-east-2", "us-west-2")) + accountID, err := awsClient.AddAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, aws.Regions("us-east-2", "us-west-2")) if err != nil { log.Fatal(err) } @@ -59,7 +59,7 @@ func main() { // Enable the exocompute feature for the account. Note that the // cnpAccountID and exoAccountID should be the same, they refer to the same // Polaris cloud account. - exoAccountID, err := awsClient.AddAccount(ctx, aws.Default(), core.FeatureExocompute, aws.Regions("us-east-2")) + exoAccountID, err := awsClient.AddAccount(ctx, aws.Default(), []core.Feature{core.FeatureExocompute}, aws.Regions("us-east-2")) if err != nil { log.Fatal(err) } @@ -100,13 +100,13 @@ func main() { } // Disable the exocompute feature for the account. - err = awsClient.RemoveAccount(ctx, aws.Default(), core.FeatureExocompute, false) + err = awsClient.RemoveAccount(ctx, aws.Default(), []core.Feature{core.FeatureExocompute}, false) if err != nil { log.Fatal(err) } // Remove the AWS account from Polaris. - err = awsClient.RemoveAccount(ctx, aws.Default(), core.FeatureCloudNativeProtection, false) + err = awsClient.RemoveAccount(ctx, aws.Default(), []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { log.Fatal(err) } diff --git a/pkg/polaris/aws/account.go b/pkg/polaris/aws/account.go index e4e389a6..a03d2be0 100644 --- a/pkg/polaris/aws/account.go +++ b/pkg/polaris/aws/account.go @@ -25,6 +25,8 @@ import ( "errors" "fmt" + graphqlaws "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" @@ -33,9 +35,10 @@ import ( ) type account struct { + cloud graphqlaws.Cloud id string name string - config aws.Config + config *aws.Config } // AccountFunc returns an account initialized from the values passed to the @@ -55,7 +58,7 @@ func Config(config aws.Config) AccountFunc { name = id } - return account{id: id, name: name, config: config}, nil + return account{id: id, name: name, config: &config}, nil } } @@ -154,7 +157,7 @@ func ProfileWithRegionAndRole(profile, region, roleARN string) AccountFunc { name = id + " : " + profile } - return account{id: id, name: name, config: config}, nil + return account{cloud: graphqlaws.CloudStandard, id: id, name: name, config: &config}, nil } } @@ -176,3 +179,26 @@ func awsAccountInfo(ctx context.Context, config aws.Config) (string, string, err return *callerID.Account, *info.Account.Name, nil } + +// Account returns an AccountFunc that initializes the account with specified +// cloud type and AWS account id. +func Account(cloud, awsAccountID string) AccountFunc { + return AccountWithName(cloud, awsAccountID, awsAccountID) +} + +// AccountWithName returns an AccountFunc that initializes the account with +// specified cloud type, AWS account id and account name. +func AccountWithName(cloud, awsAccountID, name string) AccountFunc { + return func(ctx context.Context) (account, error) { + c, err := graphqlaws.ParseCloud(cloud) + if err != nil { + return account{}, fmt.Errorf("failed to parse cloud: %s", err) + } + + if !verifyAccountID(awsAccountID) { + return account{}, fmt.Errorf("invalid AWS account id") + } + + return account{cloud: c, id: awsAccountID, name: name}, nil + } +} diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 9c82aebd..33b76995 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "net/url" + "sort" "strings" "time" @@ -39,6 +40,10 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) +const ( + requestTimeout = 5 * time.Second +) + // API for AWS account management. type API struct { client *graphql.Client @@ -57,6 +62,7 @@ func Wrap(client *polaris.Client) API { // CloudAccount for Amazon Web Services accounts. type CloudAccount struct { + Cloud string ID uuid.UUID NativeID string Name string @@ -176,6 +182,7 @@ func toCloudAccount(accountWithFeatures aws.CloudAccountWithFeatures) CloudAccou } return CloudAccount{ + Cloud: string(accountWithFeatures.Account.Cloud), ID: accountWithFeatures.Account.ID, NativeID: accountWithFeatures.Account.NativeID, Name: accountWithFeatures.Account.Name, @@ -247,15 +254,15 @@ func (a API) Accounts(ctx context.Context, feature core.Feature, filter string) return accounts, nil } -// AddAccount adds the AWS account to RSC for the given feature. Returns the RSC -// cloud account id of the added account. If name isn't given as an option it's -// derived from information in the cloud. The result can vary slightly depending -// on permissions. +// AddAccount adds the AWS account to RSC for the given features. Returns the +// RSC cloud account id of the added account. If name isn't given as an option +// it's derived from information in the cloud. The result can vary slightly +// depending on AWS permissions. // // If adding the account fails due to permission problems when creating the // CloudFormation stack, it's safe to call AddAccount again with the same // parameters after the permission problems have been resolved. -func (a API) AddAccount(ctx context.Context, account AccountFunc, feature core.Feature, opts ...OptionFunc) (uuid.UUID, error) { +func (a API) AddAccount(ctx context.Context, account AccountFunc, features []core.Feature, opts ...OptionFunc) (uuid.UUID, error) { a.log.Print(log.Trace) if account == nil { @@ -263,20 +270,20 @@ func (a API) AddAccount(ctx context.Context, account AccountFunc, feature core.F } config, err := account(ctx) if err != nil { - return uuid.Nil, fmt.Errorf("failed to lookup account: %v", err) + return uuid.Nil, fmt.Errorf("failed to lookup account: %s", err) } var options options for _, option := range opts { if err := option(ctx, &options); err != nil { - return uuid.Nil, fmt.Errorf("failed to lookup option: %v", err) + return uuid.Nil, fmt.Errorf("failed to lookup option: %s", err) } } if options.name != "" { config.name = options.name } - // If there already is a RSC cloud account for the given AWS account we use + // If there already is an RSC cloud account for the given AWS account we use // the same account name when adding the feature. RSC does not allow the // name to change between features. akkount, err := a.Account(ctx, AccountID(config.id), core.FeatureAll) @@ -284,118 +291,156 @@ func (a API) AddAccount(ctx context.Context, account AccountFunc, feature core.F config.name = akkount.Name } if err != nil && !errors.Is(err, graphql.ErrNotFound) { - return uuid.Nil, fmt.Errorf("failed to get account: %v", err) - } - - accountInit, err := aws.Wrap(a.client).ValidateAndCreateCloudAccount(ctx, config.id, config.name, feature) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to validate account: %v", err) + return uuid.Nil, fmt.Errorf("failed to get account: %s", err) } - err = aws.Wrap(a.client).FinalizeCloudAccountProtection(ctx, config.id, config.name, feature, options.regions, accountInit) - if err != nil { - return uuid.Nil, fmt.Errorf("failed to add account: %v", err) + if config.config != nil { + err = a.addAccountWithCFT(ctx, features, config, options) + } else { + err = a.addAccount(ctx, features, config, options) } - - err = awsUpdateStack(ctx, a.client.Log(), config.config, accountInit.StackName, accountInit.TemplateURL) if err != nil { - return uuid.Nil, fmt.Errorf("failed to update CloudFormation stack: %v", err) + return uuid.Nil, err } // 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), feature) + akkount, err = a.Account(ctx, AccountID(config.id), core.FeatureAll) if err != nil { - return uuid.Nil, fmt.Errorf("failed to get account: %v", err) + return uuid.Nil, fmt.Errorf("failed to get account: %s", err) } } return akkount.ID, nil } +func (a API) addAccount(ctx context.Context, features []core.Feature, config account, options options) error { + a.log.Print(log.Trace) + + accountInit := aws.CloudAccountInitiate{ + CloudFormationURL: "", + ExternalID: "", + FeatureVersions: []aws.FeatureVersion{}, + StackName: "", + TemplateURL: "", + } + + err := aws.Wrap(a.client).FinalizeCloudAccountProtection(ctx, config.cloud, config.id, config.name, features, options.regions, accountInit) + if err != nil { + return fmt.Errorf("failed to add account: %s", err) + } + + return nil +} + +func (a API) addAccountWithCFT(ctx context.Context, features []core.Feature, config account, options options) error { + a.log.Print(log.Trace) + + accountInit, err := aws.Wrap(a.client).ValidateAndCreateCloudAccount(ctx, config.id, config.name, features) + if err != nil { + return fmt.Errorf("failed to validate account: %s", err) + } + + err = aws.Wrap(a.client).FinalizeCloudAccountProtection(ctx, config.cloud, config.id, config.name, features, options.regions, accountInit) + if err != nil { + return fmt.Errorf("failed to add account: %s", err) + } + + err = awsUpdateStack(ctx, a.client.Log(), *config.config, accountInit.StackName, accountInit.TemplateURL) + if err != nil { + return fmt.Errorf("failed to update CloudFormation stack: %s", err) + } + + 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. -func (a API) RemoveAccount(ctx context.Context, account AccountFunc, feature core.Feature, deleteSnapshots bool) error { +func (a API) RemoveAccount(ctx context.Context, account AccountFunc, features []core.Feature, deleteSnapshots bool) error { a.log.Print(log.Trace) - version, err := a.client.DeploymentVersion(ctx) - if err != nil { - return fmt.Errorf("failed to get deployment version: %v", err) - } - if account == nil { return errors.New("account is not allowed to be nil") } config, err := account(ctx) if err != nil { - return fmt.Errorf("failed to lookup account: %v", err) + return fmt.Errorf("failed to lookup account: %s", err) } akkount, err := a.Account(ctx, AccountID(config.id), core.FeatureAll) if err != nil { - return fmt.Errorf("failed to get account: %v", err) + return fmt.Errorf("failed to get account: %s", err) } - rmFeature, ok := akkount.Feature(feature) - if !ok { - return fmt.Errorf("feature %v %w", rmFeature, graphql.ErrNotFound) + // Check that the account has all the features that are going to be removed. + for _, feature := range features { + if _, ok := akkount.Feature(feature); !ok { + return fmt.Errorf("feature %s %w", feature, graphql.ErrNotFound) + } } - // Disable the native (inventory) account before removing the feature. - switch { - case rmFeature.Name == core.FeatureCloudNativeProtection && rmFeature.Status != core.StatusDisabled && rmFeature.Status != core.StatusConnecting: - jobID, err := aws.Wrap(a.client).StartNativeAccountDisableJob(ctx, akkount.ID, aws.EC2, deleteSnapshots) - if err != nil { - return fmt.Errorf("failed to disable native account: %v", err) + if config.config != nil { + for _, feature := range features { + if err := a.removeAccountWithCFT(ctx, config, akkount, feature, deleteSnapshots); err != nil { + return err + } } + return nil + } - 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) - } + return a.removeAccount(ctx, akkount, features, deleteSnapshots) +} - case rmFeature.Name == core.FeatureExocompute && rmFeature.Status != core.StatusDisabled && rmFeature.Status != core.StatusConnecting: - jobID, err := aws.Wrap(a.client).StartExocomputeDisableJob(ctx, akkount.ID) - if err != nil { - return fmt.Errorf("failed to disable native account: %v", err) - } +func (a API) removeAccount(ctx context.Context, account CloudAccount, features []core.Feature, deleteSnapshots bool) error { + a.log.Print(log.Trace) - state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) - if err != nil { - return fmt.Errorf("failed to wait for taskchain: %v", err) - } - if state != core.TaskChainSucceeded { - return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) + for _, feature := range features { + if err := a.disableFeature(ctx, account, feature, deleteSnapshots); err != nil { + return fmt.Errorf("failed to disable native account: %s", err) } } - cfmURL, err := aws.Wrap(a.client).PrepareCloudAccountDeletion(ctx, akkount.ID, feature) + results, err := aws.Wrap(a.client).DeleteCloudAccountWithoutCft(ctx, account.NativeID, features) if err != nil { - return fmt.Errorf("failed to prepare to delete account: %v", err) + return fmt.Errorf("failed to delete account: %s", err) + } + var sb strings.Builder + for _, result := range results { + if !result.Success { + sb.WriteString(", ") + sb.WriteString(string(result.Feature)) + } + } + if sb.Len() > 0 { + return fmt.Errorf("failed to delete features: %s", sb.String()[2:]) } - // Determine the number of features remaining after removing one feature. - features := len(akkount.Features) - 1 + return nil +} - if version.Before("v20230628", "master-57488") { - // Having Cloud Native Protection or Exocompute implies the Cloud Accounts - // feature. - if rmFeature.Name != core.FeatureCloudAccounts { - features-- - } +func (a API) removeAccountWithCFT(ctx context.Context, config account, account CloudAccount, feature core.Feature, deleteSnapshots bool) error { + 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) + } + + cfmURL, err := aws.Wrap(a.client).PrepareCloudAccountDeletion(ctx, account.ID, feature) + if err != nil { + return fmt.Errorf("failed to prepare to delete account: %s", err) } + // Determine the number of features remaining after removing one feature. + features := len(account.Features) - 1 + // Removing the Cloud Native Protection feature implies removing the // Exocompute feature. - if rmFeature.Name == core.FeatureCloudNativeProtection { - if _, ok := akkount.Feature(core.FeatureExocompute); ok { + if feature == core.FeatureCloudNativeProtection { + if _, ok := account.Feature(core.FeatureExocompute); ok { features-- } } @@ -409,14 +454,14 @@ func (a API) RemoveAccount(ctx context.Context, account AccountFunc, feature cor u, err := url.Parse(cfmURL[i:]) if err != nil { - return fmt.Errorf("failed to parse CloudFormation url: %v", err) + return fmt.Errorf("failed to parse CloudFormation url: %s", err) } stackID := u.Query().Get("stackId") tmplURL := u.Query().Get("templateURL") - err = awsUpdateStack(ctx, a.client.Log(), config.config, stackID, tmplURL) + err = awsUpdateStack(ctx, a.client.Log(), *config.config, stackID, tmplURL) if err != nil { - return fmt.Errorf("failed to update CloudFormation stack: %v", err) + return fmt.Errorf("failed to update CloudFormation stack: %s", err) } } else { i := strings.LastIndex(cfmURL, "#/stack/detail") + 1 @@ -426,20 +471,77 @@ func (a API) RemoveAccount(ctx context.Context, account AccountFunc, feature cor u, err := url.Parse(cfmURL[i:]) if err != nil { - return fmt.Errorf("failed to parse CloudFormation url: %v", err) + return fmt.Errorf("failed to parse CloudFormation url: %s", err) } stackID := u.Query().Get("stackId") - err = awsDeleteStack(ctx, a.client.Log(), config.config, stackID) + err = awsDeleteStack(ctx, a.client.Log(), *config.config, stackID) if err != nil { - return fmt.Errorf("failed to delete CloudFormation stack: %v", err) + return fmt.Errorf("failed to delete CloudFormation stack: %s", err) } } } - err = aws.Wrap(a.client).FinalizeCloudAccountDeletion(ctx, akkount.ID, feature) + err = aws.Wrap(a.client).FinalizeCloudAccountDeletion(ctx, account.ID, feature) + if err != nil { + return fmt.Errorf("failed to delete account: %s", err) + } + + return nil +} + +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) { + return nil + } + + switch { + case rmFeature.Name == core.FeatureCloudNativeProtection: + return a.disableNativeAccount(ctx, account.ID, aws.EC2, deleteSnapshots) + + case rmFeature.Name == core.FeatureRDSProtection: + return a.disableNativeAccount(ctx, account.ID, aws.RDS, deleteSnapshots) + + case rmFeature.Name == 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) + } + + 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) + } + } + + 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 +} + +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) + } + + state, err := core.Wrap(a.client).WaitForTaskChain(ctx, jobID, 10*time.Second) if err != nil { - return fmt.Errorf("failed to delete account: %v", err) + return fmt.Errorf("failed to wait for task chain: %s", err) + } + if state != core.TaskChainSucceeded { + return fmt.Errorf("taskchain failed: jobID=%v, state=%v", jobID, state) } return nil @@ -461,6 +563,9 @@ func (a API) UpdateAccount(ctx context.Context, id IdentityFunc, feature core.Fe } account, err := a.Account(ctx, id, feature) + if errors.Is(err, graphql.ErrNotFound) { + return fmt.Errorf("failed to get account: %w", err) + } if err != nil { return fmt.Errorf("failed to get account: %v", err) } @@ -472,3 +577,152 @@ func (a API) UpdateAccount(ctx context.Context, id IdentityFunc, feature core.Fe return nil } + +const ( + roleArnSuffix = "_ROLE_ARN" + instanceProfileSuffix = "_INSTANCE_PROFILE" +) + +// Artifacts returns the artifacts, instance profiles and roles, required by RSC +// for the specified features. +func (a API) Artifacts(ctx context.Context, cloud string, features []core.Feature) ([]string, []string, error) { + a.log.Print(log.Trace) + + c, err := aws.ParseCloud(cloud) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse cloud: %s", err) + } + + artifacts, err := aws.Wrap(a.client).AllPermissionPolicies(ctx, c, features, "") + if err != nil { + return nil, nil, err + } + var profiles, roles []string + for _, artifact := range artifacts { + key := artifact.ArtifactKey + switch { + case strings.HasSuffix(key, instanceProfileSuffix): + profiles = append(profiles, strings.TrimSuffix(key, instanceProfileSuffix)) + case strings.HasSuffix(key, roleArnSuffix): + roles = append(roles, strings.TrimSuffix(key, roleArnSuffix)) + default: + a.log.Printf(log.Info, "Ignoring artifact: %s", key) + } + } + sort.Slice(profiles, func(i, j int) bool { + return profiles[i] < profiles[j] + }) + sort.Slice(roles, func(i, j int) bool { + return roles[i] < roles[j] + }) + + return profiles, roles, nil +} + +// AddAccountArtifacts adds the specified artifacts, instance profiles and +// roles, to the cloud account. +func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features []core.Feature, instanceProfiles map[string]string, roles map[string]string) (uuid.UUID, error) { + a.log.Print(log.Trace) + + if id == nil { + return uuid.Nil, errors.New("id is not allowed to be nil") + } + account, err := a.Account(ctx, id, core.FeatureAll) + if errors.Is(err, graphql.ErrNotFound) { + return uuid.Nil, fmt.Errorf("failed to get account: %w", err) + } + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get account: %s", err) + } + + externalArtifacts := make([]aws.ExternalArtifact, 0, len(instanceProfiles)+len(roles)) + for key, value := range instanceProfiles { + if !strings.HasSuffix(key, instanceProfileSuffix) { + key = key + instanceProfileSuffix + } + externalArtifacts = append(externalArtifacts, aws.ExternalArtifact{ + ExternalArtifactKey: key, + ExternalArtifactValue: value, + }) + } + for key, value := range roles { + if !strings.HasSuffix(key, roleArnSuffix) { + key = key + roleArnSuffix + } + externalArtifacts = append(externalArtifacts, aws.ExternalArtifact{ + ExternalArtifactKey: key, + ExternalArtifactValue: value, + }) + } + + var mappings []aws.NativeIDToRSCIDMapping + for i := 0; i < 7; i++ { + mappings, err = aws.Wrap(a.client).RegisterFeatureArtifacts(ctx, aws.Cloud(account.Cloud), []aws.AccountFeatureArtifact{{ + NativeID: account.NativeID, + Features: features, + Artifacts: externalArtifacts, + }}) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to register feature artifacts: %s", err) + } + if len(mappings) != 1 { + return uuid.Nil, errors.New("expected account mappings for a single account") + } + if msg := mappings[0].Message; msg == "" || !strings.Contains(msg, "RBK30300003") { + break + } + + time.Sleep(requestTimeout) + } + if msg := mappings[0].Message; msg != "" { + return uuid.Nil, fmt.Errorf("failed to register feature artifacts: %s", msg) + } + + accountID, err := uuid.Parse(mappings[0].CloudAccountID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to parse cloud account id: %s", err) + } + + return accountID, nil +} + +// TrustPolicies returns the trust policies required by RSC for the specified +// features. If the external ID is the empty, RSC will generate an external ID. +func (a API) TrustPolicies(ctx context.Context, id IdentityFunc, features []core.Feature, externalID string) (map[string]string, error) { + a.log.Print(log.Trace) + + if id == nil { + return nil, errors.New("id is not allowed to be nil") + } + account, err := a.Account(ctx, id, core.FeatureAll) + if errors.Is(err, graphql.ErrNotFound) { + return nil, fmt.Errorf("failed to get account: %w", err) + } + if err != nil { + return nil, fmt.Errorf("failed to get account: %s", err) + } + + policies, err := aws.Wrap(a.client).TrustPolicy(ctx, aws.Cloud(account.Cloud), features, []aws.TrustPolicyAccount{{ + ID: account.NativeID, + ExternalID: externalID, + }}) + if err != nil { + return nil, fmt.Errorf("failed to get trust policies: %s", err) + } + if len(policies) != 1 { + return nil, fmt.Errorf("expected trust policies for a single account") + } + + trustPolicies := make(map[string]string) + for _, artifact := range policies[0].Artifacts { + if msg := artifact.ErrorMessage; msg != "" { + return nil, fmt.Errorf("failed to get trust policies: %s", msg) + } + if strings.HasSuffix(artifact.ExternalArtifactKey, roleArnSuffix) { + artifact.ExternalArtifactKey = strings.TrimSuffix(artifact.ExternalArtifactKey, roleArnSuffix) + } + trustPolicies[artifact.ExternalArtifactKey] = artifact.TrustPolicyDoc + } + + return trustPolicies, nil +} diff --git a/pkg/polaris/aws/aws_test.go b/pkg/polaris/aws/aws_test.go index 027885f5..b65365e8 100644 --- a/pkg/polaris/aws/aws_test.go +++ b/pkg/polaris/aws/aws_test.go @@ -104,7 +104,7 @@ func TestAwsAccountAddAndRemove(t *testing.T) { // Adds the AWS account identified by the specified profile to RSC. Note // that the profile needs to have a default region. - id, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), core.FeatureCloudNativeProtection, + id, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureCloudNativeProtection}, Name(testAccount.AccountName), Regions("us-east-2")) if err != nil { t.Fatal(err) @@ -154,7 +154,7 @@ func TestAwsAccountAddAndRemove(t *testing.T) { } // Remove AWS account from RSC. - err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), core.FeatureCloudNativeProtection, false) + err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { t.Fatal(err) } @@ -196,7 +196,7 @@ func TestAwsCrossAccountAddAndRemove(t *testing.T) { // Use the default profile to add an AWS account to RSC using a cross // account role. Note that the profile needs to have a region. id, err := awsClient.AddAccount(ctx, - ProfileWithRole(testAccount.Profile, testAccount.CrossAccountRole), core.FeatureCloudNativeProtection, + ProfileWithRole(testAccount.Profile, testAccount.CrossAccountRole), []core.Feature{core.FeatureCloudNativeProtection}, Name(testAccount.CrossAccountName), Regions("us-east-2")) if err != nil { t.Fatal(err) @@ -238,7 +238,7 @@ func TestAwsCrossAccountAddAndRemove(t *testing.T) { // Remove AWS account from RSC using a cross account role. err = awsClient.RemoveAccount(ctx, ProfileWithRole(testAccount.Profile, testAccount.CrossAccountRole), - core.FeatureCloudNativeProtection, false) + []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { t.Fatal(err) } diff --git a/pkg/polaris/aws/exocompute_test.go b/pkg/polaris/aws/exocompute_test.go index 2d1a869e..01b2dac2 100644 --- a/pkg/polaris/aws/exocompute_test.go +++ b/pkg/polaris/aws/exocompute_test.go @@ -60,14 +60,14 @@ func TestAwsExocompute(t *testing.T) { // Adds the AWS account identified by the specified profile to RSC. Note // that the profile needs to have a default region. - accountID, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), core.FeatureCloudNativeProtection, + accountID, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureCloudNativeProtection}, Name(testAccount.AccountName), Regions("us-east-2")) if err != nil { t.Fatal(err) } // Enable the exocompute feature for the account. - exoAccountID, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), core.FeatureExocompute, + exoAccountID, err := awsClient.AddAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureExocompute}, Regions("us-east-2")) if err != nil { t.Fatal(err) @@ -154,7 +154,7 @@ func TestAwsExocompute(t *testing.T) { } // Disable the exocompute feature for the account. - err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), core.FeatureExocompute, false) + err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureExocompute}, false) if err != nil { t.Fatal(err) } @@ -166,7 +166,7 @@ func TestAwsExocompute(t *testing.T) { } // Remove the AWS account from RSC. - err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), core.FeatureCloudNativeProtection, false) + err = awsClient.RemoveAccount(ctx, Profile(testAccount.Profile), []core.Feature{core.FeatureCloudNativeProtection}, false) if err != nil { t.Fatal(err) } diff --git a/pkg/polaris/aws/identity.go b/pkg/polaris/aws/identity.go index 8038fe8c..4556ac89 100644 --- a/pkg/polaris/aws/identity.go +++ b/pkg/polaris/aws/identity.go @@ -45,7 +45,7 @@ type IdentityFunc func(ctx context.Context) (identity, error) func AccountID(awsAccountID string) IdentityFunc { return func(ctx context.Context) (identity, error) { if !verifyAccountID(awsAccountID) { - return identity{}, errors.New("invalid AWS id") + return identity{}, errors.New("invalid AWS account id") } return identity{id: awsAccountID, internal: false}, nil @@ -82,7 +82,7 @@ func Role(roleARN string) IdentityFunc { return identity{}, fmt.Errorf("failed to parse role ARN: %v", err) } if !verifyAccountID(arn.AccountID) { - return identity{}, errors.New("invalid AWS id") + return identity{}, errors.New("invalid AWS account id") } return identity{id: arn.AccountID, internal: false}, nil diff --git a/pkg/polaris/aws/permissions.go b/pkg/polaris/aws/permissions.go index 5376487c..5e85e190 100644 --- a/pkg/polaris/aws/permissions.go +++ b/pkg/polaris/aws/permissions.go @@ -1,3 +1,23 @@ +// 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 ( @@ -5,6 +25,7 @@ import ( "errors" "fmt" "net/url" + "sort" "strings" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/aws" @@ -12,10 +33,74 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) +// CustomerManagedPolicy represents a policy that is managed by the customer. +type CustomerManagedPolicy struct { + Artifact string + Feature core.Feature + Name string + Policy string +} + +func (policy CustomerManagedPolicy) lessThan(other CustomerManagedPolicy) bool { + return policy.Artifact < other.Artifact || policy.Feature < other.Feature || policy.Name < other.Name +} + +// ManagedPolicy represents a policy that is managed by AWS. +type ManagedPolicy struct { + Artifact string + Name string +} + +func (policy ManagedPolicy) lessThan(other ManagedPolicy) bool { + return policy.Artifact < other.Artifact || policy.Name < other.Name +} + +// Permissions returns the policies required by RSC for the specified features. +func (a API) Permissions(ctx context.Context, cloud string, features []core.Feature, ec2RecoveryRolePath string) ([]CustomerManagedPolicy, []ManagedPolicy, error) { + a.log.Print(log.Trace) + + c, err := aws.ParseCloud(cloud) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse cloud: %s", err) + } + + artifacts, err := aws.Wrap(a.client).AllPermissionPolicies(ctx, c, features, ec2RecoveryRolePath) + if err != nil { + return nil, nil, err + } + + var customerPolicies []CustomerManagedPolicy + var managedPolicies []ManagedPolicy + for _, artifact := range artifacts { + for _, policy := range artifact.CustomerManagedPolicies { + customerPolicies = append(customerPolicies, CustomerManagedPolicy{ + Artifact: strings.TrimSuffix(artifact.ArtifactKey, roleArnSuffix), + Feature: policy.Feature, + Name: policy.PolicyName, + Policy: policy.PolicyDocument, + }) + } + for _, policy := range artifact.ManagedPolicies { + managedPolicies = append(managedPolicies, ManagedPolicy{ + Artifact: strings.TrimSuffix(artifact.ArtifactKey, roleArnSuffix), + Name: policy, + }) + } + } + sort.Slice(customerPolicies, func(i, j int) bool { + return customerPolicies[i].lessThan(customerPolicies[j]) + }) + sort.Slice(managedPolicies, func(i, j int) bool { + return managedPolicies[i].lessThan(managedPolicies[j]) + }) + + return customerPolicies, managedPolicies, nil +} + // UpdatePermissions updates the permissions of the CloudFormation stack in // AWS. func (a API) UpdatePermissions(ctx context.Context, account AccountFunc, features []core.Feature) error { - a.client.Log().Print(log.Trace) + a.log.Print(log.Trace) if account == nil { return errors.New("account is not allowed to be nil") @@ -25,6 +110,10 @@ func (a API) UpdatePermissions(ctx context.Context, account AccountFunc, feature return fmt.Errorf("failed to lookup account: %v", err) } + if config.config == nil { + return fmt.Errorf("only applicable to cloud accounts using cft") + } + akkount, err := a.Account(ctx, AccountID(config.id), core.FeatureAll) if err != nil { return fmt.Errorf("failed to get account: %v", err) @@ -47,7 +136,7 @@ func (a API) UpdatePermissions(ctx context.Context, account AccountFunc, feature } stackID := u.Query().Get("stackId") - err = awsUpdateStack(ctx, a.client.Log(), config.config, stackID, tmplURL) + err = awsUpdateStack(ctx, a.client.Log(), *config.config, stackID, tmplURL) if err != nil { return fmt.Errorf("failed to update CloudFormation stack: %v", err) } diff --git a/pkg/polaris/graphql/aws/aws.go b/pkg/polaris/graphql/aws/aws.go index e885ac1c..facac138 100644 --- a/pkg/polaris/graphql/aws/aws.go +++ b/pkg/polaris/graphql/aws/aws.go @@ -25,7 +25,7 @@ package aws import ( - "errors" + "fmt" "strings" "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql" @@ -36,10 +36,30 @@ import ( type Cloud string const ( - ChinaCloud Cloud = "CHINA" - StandardCloud Cloud = "STANDARD" + ChinaCloud Cloud = "CHINA" // Deprecated: use CloudChina. + GovCloud Cloud = "GOV" // Deprecated: use CloudGov. + StandardCloud Cloud = "STANDARD" // Deprecated: use CloudStandard. ) +const ( + CloudC2S Cloud = "C2S" + CloudChina Cloud = "CHINA" + CloudGov Cloud = "GOV" + CloudSC2S Cloud = "SC2S" + CloudStandard Cloud = "STANDARD" +) + +// ParseCloud returns the Cloud matching the given cloud string. +func ParseCloud(cloud string) (Cloud, error) { + c := Cloud(strings.ToUpper(cloud)) + switch c { + case CloudC2S, CloudChina, CloudGov, CloudSC2S, CloudStandard: + return c, nil + default: + return CloudStandard, fmt.Errorf("invalid cloud: %s", cloud) + } +} + // ProtectionFeature represents the protection features of an AWS cloud // account. type ProtectionFeature string @@ -58,13 +78,16 @@ const ( RegionApEast1 Region = "AP_EAST_1" RegionApNorthEast1 Region = "AP_NORTHEAST_1" RegionApNorthEast2 Region = "AP_NORTHEAST_2" + RegionApNorthEast3 Region = "AP_NORTHEAST_3" RegionApSouthEast1 Region = "AP_SOUTHEAST_1" RegionApSouthEast2 Region = "AP_SOUTHEAST_2" + RegionApSouthEast3 Region = "AP_SOUTHEAST_3" RegionApSouth1 Region = "AP_SOUTH_1" RegionCaCentral1 Region = "CA_CENTRAL_1" RegionCnNorthWest1 Region = "CN_NORTHWEST_1" RegionCnNorth1 Region = "CN_NORTH_1" RegionEuCentral1 Region = "EU_CENTRAL_1" + RegionEuCentral2 Region = "EU_CENTRAL_2" RegionEuNorth1 Region = "EU_NORTH_1" RegionEuSouth1 Region = "EU_SOUTH_1" RegionEuWest1 Region = "EU_WEST_1" @@ -74,6 +97,8 @@ const ( RegionSaEast1 Region = "SA_EAST_1" RegionUsEast1 Region = "US_EAST_1" RegionUsEast2 Region = "US_EAST_2" + RegionUsGovEast1 Region = "US_GOV_EAST_1" + RegionUsGovWest1 Region = "US_GOV_WEST_1" RegionUsWest1 Region = "US_WEST_1" RegionUsWest2 Region = "US_WEST_2" ) @@ -100,13 +125,16 @@ var validRegions = map[Region]struct{}{ RegionApEast1: {}, RegionApNorthEast1: {}, RegionApNorthEast2: {}, + RegionApNorthEast3: {}, RegionApSouthEast1: {}, RegionApSouthEast2: {}, + RegionApSouthEast3: {}, RegionApSouth1: {}, RegionCaCentral1: {}, RegionCnNorthWest1: {}, RegionCnNorth1: {}, RegionEuCentral1: {}, + RegionEuCentral2: {}, RegionEuNorth1: {}, RegionEuSouth1: {}, RegionEuWest1: {}, @@ -116,6 +144,8 @@ var validRegions = map[Region]struct{}{ RegionSaEast1: {}, RegionUsEast1: {}, RegionUsEast2: {}, + RegionUsGovEast1: {}, + RegionUsGovWest1: {}, RegionUsWest1: {}, RegionUsWest2: {}, } @@ -135,7 +165,7 @@ func ParseRegion(region string) (Region, error) { return r, nil } - return RegionUnknown, errors.New("invalid aws region") + return RegionUnknown, fmt.Errorf("invalid aws region: %s", region) } // ParseRegions returns the Regions matching the given regions. Accepts both diff --git a/pkg/polaris/graphql/aws/cloud.go b/pkg/polaris/graphql/aws/cloud.go index c371c3d2..b3e79ab7 100644 --- a/pkg/polaris/graphql/aws/cloud.go +++ b/pkg/polaris/graphql/aws/cloud.go @@ -134,18 +134,18 @@ type CloudAccountInitiate struct { // account to RSC. The returned CloudAccountInitiate value must be passed on to // FinalizeCloudAccountProtection which is the next step in the process of // adding an AWS account to RSC. -func (a API) ValidateAndCreateCloudAccount(ctx context.Context, id, name string, feature core.Feature) (CloudAccountInitiate, error) { +func (a API) ValidateAndCreateCloudAccount(ctx context.Context, id, name string, features []core.Feature) (CloudAccountInitiate, error) { a.log.Print(log.Trace) buf, err := a.GQL.Request(ctx, validateAndCreateAwsCloudAccountQuery, struct { ID string `json:"nativeId"` Name string `json:"accountName"` Features []core.Feature `json:"features"` - }{ID: id, Name: name, Features: []core.Feature{feature}}) + }{ID: id, Name: name, Features: features}) if err != nil { return CloudAccountInitiate{}, fmt.Errorf("failed to request validateAndCreateAwsCloudAccount: %w", err) } - a.log.Printf(log.Debug, "validateAndCreateAwsCloudAccount(%q, %q, %q): %s", id, name, feature, string(buf)) + a.log.Printf(log.Debug, "validateAndCreateAwsCloudAccount(%q, %q, %v): %s", id, name, features, string(buf)) var payload struct { Data struct { @@ -183,23 +183,24 @@ func (a API) ValidateAndCreateCloudAccount(ctx context.Context, id, name string, // specified AWS account to RSC. The message returned by the GraphQL API is // converted into a Go error. After this function a CloudFormation stack must // be created using the information returned by ValidateAndCreateCloudAccount. -func (a API) FinalizeCloudAccountProtection(ctx context.Context, id, name string, feature core.Feature, regions []Region, init CloudAccountInitiate) error { +func (a API) FinalizeCloudAccountProtection(ctx context.Context, cloud Cloud, id, name string, features []core.Feature, regions []Region, init CloudAccountInitiate) error { a.log.Print(log.Trace) buf, err := a.GQL.Request(ctx, finalizeAwsCloudAccountProtectionQuery, struct { + Cloud Cloud `json:"cloudType"` ID string `json:"nativeId"` Name string `json:"accountName"` Regions []Region `json:"awsRegions,omitempty"` ExternalID string `json:"externalId"` FeatureVersion []FeatureVersion `json:"featureVersion"` - Feature core.Feature `json:"feature"` + Features []core.Feature `json:"features"` StackName string `json:"stackName"` - }{ID: id, Name: name, Regions: regions, ExternalID: init.ExternalID, FeatureVersion: init.FeatureVersions, Feature: feature, StackName: init.StackName}) + }{Cloud: cloud, ID: id, Name: name, Regions: regions, ExternalID: init.ExternalID, FeatureVersion: init.FeatureVersions, Features: features, StackName: init.StackName}) if err != nil { return fmt.Errorf("failed to request finalizeAwsCloudAccountProtection: %w", err) } - a.log.Printf(log.Debug, "finalizeAwsCloudAccountProtection(%q, %q, %q, %q, %v, %q, %q): %s", id, name, regions, init.ExternalID, - init.FeatureVersions, feature, init.StackName, string(buf)) + a.log.Printf(log.Debug, "finalizeAwsCloudAccountProtection(%q, %q, %q, %q, %v, %v, %q): %s", id, name, regions, init.ExternalID, + init.FeatureVersions, features, init.StackName, string(buf)) var payload struct { Data struct { @@ -221,6 +222,9 @@ func (a API) FinalizeCloudAccountProtection(ctx context.Context, id, name string if !strings.HasPrefix(strings.ToLower(payload.Data.Query.Message), "successfully") { return errors.New(payload.Data.Query.Message) } + if len(payload.Data.Query.AwsChildAccounts) != 1 { + return errors.New("expected a single aws child account") + } return nil } diff --git a/pkg/polaris/graphql/aws/cloud_no_cft.go b/pkg/polaris/graphql/aws/cloud_no_cft.go new file mode 100644 index 00000000..3954781b --- /dev/null +++ b/pkg/polaris/graphql/aws/cloud_no_cft.go @@ -0,0 +1,199 @@ +// 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" + "fmt" + + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/graphql/core" + "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" +) + +// PermissionPolicyArtifact holds the permission policies for a specific cloud +// and feature set. +type PermissionPolicyArtifact struct { + ArtifactKey string `json:"externalArtifactKey"` + ManagedPolicies []string `json:"awsManagedPolicies"` + CustomerManagedPolicies []struct { + Feature core.Feature `json:"feature"` + PolicyName string `json:"policyName"` + PolicyDocument string `json:"policyDocumentJson"` + } `json:"customerManagedPolicies"` +} + +// AllPermissionPolicies returns all permission policies for the specified cloud +// and feature set. +func (a API) AllPermissionPolicies(ctx context.Context, cloud Cloud, features []core.Feature, ec2RecoveryRolePath string) ([]PermissionPolicyArtifact, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, allAwsPermissionPoliciesQuery, struct { + Cloud Cloud `json:"cloudType"` + Features []core.Feature `json:"features"` + RolePath string `json:"ec2RecoveryRolePath,omitempty"` + }{Cloud: cloud, Features: features, RolePath: ec2RecoveryRolePath}) + if err != nil { + return nil, fmt.Errorf("failed to request allAwsPermissionPolicies: %w", err) + } + + var payload struct { + Data struct { + Result []PermissionPolicyArtifact `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal allAwsPermissionPolicies: %v", err) + } + + return payload.Data.Result, nil +} + +// TrustPolicyAccount holds the native ID and external ID. +type TrustPolicyAccount struct { + ID string `json:"id"` + ExternalID string `json:"externalId,omitempty"` +} + +// TrustPolicy holds the native ID and the artifacts. +type TrustPolicy struct { + NativeID string `json:"awsNativeId"` + Artifacts []TrustPolicyArtifact `json:"artifacts"` +} + +// TrustPolicyArtifact holds the artifact key and the corresponding trust policy +// document. If an error occurs ErrorMessage will be set. +type TrustPolicyArtifact struct { + ExternalArtifactKey string `json:"externalArtifactKey"` + TrustPolicyDoc string `json:"trustPolicyDoc"` + ErrorMessage string `json:"errorMessage"` +} + +// TrustPolicy returns the trust policy for the specified account and external +// id. +func (a API) TrustPolicy(ctx context.Context, cloud Cloud, features []core.Feature, trustPolicyAccounts []TrustPolicyAccount) ([]TrustPolicy, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, awsTrustPolicyQuery, struct { + Cloud Cloud `json:"cloudType"` + Features []core.Feature `json:"features"` + NativeAccounts []TrustPolicyAccount `json:"awsNativeAccounts"` + }{Cloud: cloud, Features: features, NativeAccounts: trustPolicyAccounts}) + if err != nil { + return nil, fmt.Errorf("failed to request awsTrustPolicy: %w", err) + } + + var payload struct { + Data struct { + Result struct { + TrustPolicies []TrustPolicy `json:"result"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal awsTrustPolicy: %v", err) + } + + return payload.Data.Result.TrustPolicies, nil +} + +// AccountFeatureArtifact holds the artifacts for a cloud account, identified by +// the native ID. +type AccountFeatureArtifact struct { + NativeID string `json:"awsNativeId"` + Features []core.Feature `json:"features"` + Artifacts []ExternalArtifact `json:"externalArtifacts"` +} + +// ExternalArtifact holds the key and value for an artifact. +type ExternalArtifact struct { + ExternalArtifactKey string `json:"externalArtifactKey"` + ExternalArtifactValue string `json:"externalArtifactValue"` +} + +// NativeIDToRSCIDMapping holds a mapping between cloud account ID and native +// ID. If an error occurs Message will be set. +type NativeIDToRSCIDMapping struct { + CloudAccountID string `json:"awsCloudAccountId"` + NativeID string `json:"awsNativeId"` + Message string `json:"Message"` +} + +// RegisterFeatureArtifacts registers the specified artifacts with the cloud +// account identified by the native ID. +func (a API) RegisterFeatureArtifacts(ctx context.Context, cloud Cloud, artifacts []AccountFeatureArtifact) ([]NativeIDToRSCIDMapping, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, registerAwsFeatureArtifactsQuery, struct { + Cloud Cloud `json:"cloudType"` + Artifacts []AccountFeatureArtifact `json:"awsArtifacts"` + }{Cloud: cloud, Artifacts: artifacts}) + if err != nil { + return nil, fmt.Errorf("failed to request registerAwsFeatureArtifacts: %w", err) + } + + var payload struct { + Data struct { + Result struct { + Mappings []NativeIDToRSCIDMapping `json:"allAwsNativeIdtoRscIdMappings"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal registerAwsFeatureArtifacts: %v", err) + } + + return payload.Data.Result.Mappings, nil +} + +// FeatureResult gives the result of the delete operation for a feature. +type FeatureResult struct { + Feature core.Feature + Success bool +} + +// DeleteCloudAccountWithoutCft deletes the cloud account identified by the +// native ID. Note that certain features needs to be disabled before being +// deleted. +func (a API) DeleteCloudAccountWithoutCft(ctx context.Context, nativeID string, features []core.Feature) ([]FeatureResult, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, bulkDeleteAwsCloudAccountWithoutCftQuery, struct { + NativeID string `json:"awsNativeId"` + Features []core.Feature `json:"features"` + }{NativeID: nativeID, Features: features}) + if err != nil { + return nil, fmt.Errorf("failed to request bulkDeleteAwsCloudAccountWithoutCft: %w", err) + } + + var payload struct { + Data struct { + Result struct { + FeatureResult []FeatureResult `json:"deleteAwsCloudAccountWithoutCftResp"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal bulkDeleteAwsCloudAccountWithoutCft: %v", err) + } + + return payload.Data.Result.FeatureResult, nil +} diff --git a/pkg/polaris/graphql/aws/cloud_test.go b/pkg/polaris/graphql/aws/cloud_test.go index 9b5cfe40..48c0aed9 100644 --- a/pkg/polaris/graphql/aws/cloud_test.go +++ b/pkg/polaris/graphql/aws/cloud_test.go @@ -70,7 +70,7 @@ func TestValidateAndCreateAWSCloudAccountWithDuplicate(t *testing.T) { defer srv.Shutdown(context.Background()) _, err := Wrap(client).ValidateAndCreateCloudAccount(context.Background(), - "123456789012", "123456789012 : default", core.FeatureCloudNativeProtection) + "123456789012", "123456789012 : default", []core.Feature{core.FeatureCloudNativeProtection}) if err == nil { t.Fatal("expected ValidateAndCreateCloudAccount to fail") } diff --git a/pkg/polaris/graphql/aws/queries.go b/pkg/polaris/graphql/aws/queries.go index 213804b5..61a2372a 100644 --- a/pkg/polaris/graphql/aws/queries.go +++ b/pkg/polaris/graphql/aws/queries.go @@ -82,6 +82,25 @@ var allAwsExocomputeConfigsQuery = `query SdkGolangAllAwsExocomputeConfigs($awsN } }` +// allAwsPermissionPolicies GraphQL query +var allAwsPermissionPoliciesQuery = `query SdkGolangAllAwsPermissionPolicies($cloudType: AwsCloudType!, $features: [CloudAccountFeature!]!, $ec2RecoveryRolePath: String) { + result: allAwsPermissionPolicies(input: { + cloudType: $cloudType, + features: $features, + featureSpecificDetails: { + ec2RecoveryRolePath: $ec2RecoveryRolePath + } + }) { + externalArtifactKey + awsManagedPolicies + customerManagedPolicies { + feature + policyName + policyDocumentJson + } + } +}` + // allVpcsByRegionFromAws GraphQL query var allVpcsByRegionFromAwsQuery = `query SdkGolangAllVpcsByRegionFromAws($awsAccountRubrikId: UUID!, $region: AwsNativeRegion!) { allVpcsByRegionFromAws(awsAccountRubrikId: $awsAccountRubrikId, region: $region) { @@ -172,6 +191,30 @@ var awsNativeAccountsQuery = `query SdkGolangAwsNativeAccounts($after: String, $ } }` +// awsTrustPolicy GraphQL query +var awsTrustPolicyQuery = `query SdkGolangAwsTrustPolicy($cloudType: AwsCloudType!, $features: [CloudAccountFeature!]!, $awsNativeAccounts: [AwsNativeAccountInput!]!) { + result: awsTrustPolicy(input: {cloudType: $cloudType, features: $features, awsNativeAccounts: $awsNativeAccounts}) { + result { + artifacts { + externalArtifactKey + trustPolicyDoc + errorMessage + } + awsNativeId + } + } +}` + +// bulkDeleteAwsCloudAccountWithoutCft GraphQL query +var bulkDeleteAwsCloudAccountWithoutCftQuery = `mutation SdkGolangBulkDeleteAwsCloudAccountWithoutCft($awsNativeId: String!, $features: [CloudAccountFeature!]) { + result: bulkDeleteAwsCloudAccountWithoutCft(input: {awsNativeId: $awsNativeId, features: $features}) { + deleteAwsCloudAccountWithoutCftResp { + feature + success + } + } +}` + // createAwsExocomputeConfigs GraphQL query var createAwsExocomputeConfigsQuery = `mutation SdkGolangCreateAwsExocomputeConfigs($cloudAccountId: UUID!, $configs: [AwsExocomputeConfigInput!]!) { createAwsExocomputeConfigs(input: {cloudAccountId: $cloudAccountId, configs: $configs}) { @@ -213,7 +256,7 @@ var finalizeAwsCloudAccountDeletionQuery = `mutation SdkGolangFinalizeAwsCloudAc }` // finalizeAwsCloudAccountProtection GraphQL query -var finalizeAwsCloudAccountProtectionQuery = `mutation SdkGolangFinalizeAwsCloudAccountProtection($nativeId: String!, $accountName: String!, $awsRegions: [AwsCloudAccountRegion!], $externalId: String!, $featureVersion: [AwsCloudAccountFeatureVersionInput!]!, $feature: CloudAccountFeature!, $stackName: String!) { +var finalizeAwsCloudAccountProtectionQuery = `mutation SdkGolangFinalizeAwsCloudAccountProtection($nativeId: String!, $accountName: String!, $awsRegions: [AwsCloudAccountRegion!], $externalId: String!, $featureVersion: [AwsCloudAccountFeatureVersionInput!]!, $features: [CloudAccountFeature!]!, $stackName: String!) { finalizeAwsCloudAccountProtection(input: { action: CREATE, awsChildAccounts: [{ @@ -223,7 +266,7 @@ var finalizeAwsCloudAccountProtectionQuery = `mutation SdkGolangFinalizeAwsCloud awsRegions: $awsRegions, externalId: $externalId, featureVersion: $featureVersion, - features: [$feature], + features: $features, stackName: $stackName, }) { awsChildAccounts { @@ -250,6 +293,17 @@ var prepareFeatureUpdateForAwsCloudAccountQuery = `mutation SdkGolangPrepareFeat } }` +// registerAwsFeatureArtifacts GraphQL query +var registerAwsFeatureArtifactsQuery = `mutation SdkGolangRegisterAwsFeatureArtifacts($cloudType: AwsCloudType, $awsArtifacts: [AwsAccountFeatureArtifact!]!) { + result: registerAwsFeatureArtifacts(input: {cloudType: $cloudType, awsArtifacts: $awsArtifacts}) { + allAwsNativeIdtoRscIdMappings { + awsCloudAccountId + awsNativeId + message + } + } +}` + // startAwsExocomputeDisableJob GraphQL query var startAwsExocomputeDisableJobQuery = `mutation SdkGolangStartAwsExocomputeDisableJob($cloudAccountId: UUID!) { result: startAwsExocomputeDisableJob(input: {cloudAccountId: $cloudAccountId}) { diff --git a/pkg/polaris/graphql/aws/queries/all_aws_permission_policies.graphql b/pkg/polaris/graphql/aws/queries/all_aws_permission_policies.graphql new file mode 100644 index 00000000..37f03eda --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/all_aws_permission_policies.graphql @@ -0,0 +1,17 @@ +query RubrikPolarisSDKRequest($cloudType: AwsCloudType!, $features: [CloudAccountFeature!]!, $ec2RecoveryRolePath: String) { + result: allAwsPermissionPolicies(input: { + cloudType: $cloudType, + features: $features, + featureSpecificDetails: { + ec2RecoveryRolePath: $ec2RecoveryRolePath + } + }) { + externalArtifactKey + awsManagedPolicies + customerManagedPolicies { + feature + policyName + policyDocumentJson + } + } +} diff --git a/pkg/polaris/graphql/aws/queries/aws_trust_policy.graphql b/pkg/polaris/graphql/aws/queries/aws_trust_policy.graphql new file mode 100644 index 00000000..0844ca19 --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/aws_trust_policy.graphql @@ -0,0 +1,12 @@ +query RubrikPolarisSDKRequest($cloudType: AwsCloudType!, $features: [CloudAccountFeature!]!, $awsNativeAccounts: [AwsNativeAccountInput!]!) { + result: awsTrustPolicy(input: {cloudType: $cloudType, features: $features, awsNativeAccounts: $awsNativeAccounts}) { + result { + artifacts { + externalArtifactKey + trustPolicyDoc + errorMessage + } + awsNativeId + } + } +} diff --git a/pkg/polaris/graphql/aws/queries/bulk_delete_aws_cloud_account_without_cft.graphql b/pkg/polaris/graphql/aws/queries/bulk_delete_aws_cloud_account_without_cft.graphql new file mode 100644 index 00000000..d44dfe99 --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/bulk_delete_aws_cloud_account_without_cft.graphql @@ -0,0 +1,8 @@ +mutation RubrikPolarisSDKRequest($awsNativeId: String!, $features: [CloudAccountFeature!]) { + result: bulkDeleteAwsCloudAccountWithoutCft(input: {awsNativeId: $awsNativeId, features: $features}) { + deleteAwsCloudAccountWithoutCftResp { + feature + success + } + } +} diff --git a/pkg/polaris/graphql/aws/queries/finalize_aws_cloud_account_protection.graphql b/pkg/polaris/graphql/aws/queries/finalize_aws_cloud_account_protection.graphql index 2b423b6f..cc40b0dd 100644 --- a/pkg/polaris/graphql/aws/queries/finalize_aws_cloud_account_protection.graphql +++ b/pkg/polaris/graphql/aws/queries/finalize_aws_cloud_account_protection.graphql @@ -1,4 +1,4 @@ -mutation RubrikPolarisSDKRequest($nativeId: String!, $accountName: String!, $awsRegions: [AwsCloudAccountRegion!], $externalId: String!, $featureVersion: [AwsCloudAccountFeatureVersionInput!]!, $feature: CloudAccountFeature!, $stackName: String!) { +mutation RubrikPolarisSDKRequest($nativeId: String!, $accountName: String!, $awsRegions: [AwsCloudAccountRegion!], $externalId: String!, $featureVersion: [AwsCloudAccountFeatureVersionInput!]!, $features: [CloudAccountFeature!]!, $stackName: String!) { finalizeAwsCloudAccountProtection(input: { action: CREATE, awsChildAccounts: [{ @@ -8,7 +8,7 @@ mutation RubrikPolarisSDKRequest($nativeId: String!, $accountName: String!, $aws awsRegions: $awsRegions, externalId: $externalId, featureVersion: $featureVersion, - features: [$feature], + features: $features, stackName: $stackName, }) { awsChildAccounts { diff --git a/pkg/polaris/graphql/aws/queries/register_aws_feature_artifacts.graphql b/pkg/polaris/graphql/aws/queries/register_aws_feature_artifacts.graphql new file mode 100644 index 00000000..fa42ffcf --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/register_aws_feature_artifacts.graphql @@ -0,0 +1,9 @@ +mutation RubrikPolarisSDKRequest($cloudType: AwsCloudType, $awsArtifacts: [AwsAccountFeatureArtifact!]!) { + result: registerAwsFeatureArtifacts(input: {cloudType: $cloudType, awsArtifacts: $awsArtifacts}) { + allAwsNativeIdtoRscIdMappings { + awsCloudAccountId + awsNativeId + message + } + } +} diff --git a/pkg/polaris/graphql/core/core.go b/pkg/polaris/graphql/core/core.go index 8911075a..7590d1cb 100644 --- a/pkg/polaris/graphql/core/core.go +++ b/pkg/polaris/graphql/core/core.go @@ -33,7 +33,6 @@ import ( "time" "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/log" ) @@ -57,25 +56,49 @@ const ( FeatureAll Feature = "ALL" FeatureAppFlows Feature = "APP_FLOWS" FeatureArchival Feature = "ARCHIVAL" - FeatureCloudAccounts Feature = "CLOUDACCOUNTS" + FeatureAzureSQLDBProtection Feature = "AZURE_SQL_DB_PROTECTION" + FeatureAzureSQLMIProtection Feature = "AZURE_SQL_MI_PROTECTION" + FeatureCloudAccounts Feature = "CLOUDACCOUNTS" // Deprecated, no replacement. FeatureCloudNativeArchival Feature = "CLOUD_NATIVE_ARCHIVAL" FeatureCloudNativeArchivalEncryption Feature = "CLOUD_NATIVE_ARCHIVAL_ENCRYPTION" + FeatureCloudNativeBLOBProtection Feature = "CLOUD_NATIVE_BLOB_PROTECTION" FeatureCloudNativeProtection Feature = "CLOUD_NATIVE_PROTECTION" + FeatureCloudNativeS3Protection Feature = "CLOUD_NATIVE_S3_PROTECTION" FeatureExocompute Feature = "EXOCOMPUTE" FeatureGCPSharedVPCHost Feature = "GCP_SHARED_VPC_HOST" + FeatureServerAndApps Feature = "SERVERS_AND_APPS" FeatureRDSProtection Feature = "RDS_PROTECTION" + FeatureKubernetesProtection Feature = "KUBERNETES_PROTECTION" ) var validFeatures = map[Feature]struct{}{ - FeatureAll: {}, - FeatureAppFlows: {}, - FeatureArchival: {}, - FeatureCloudAccounts: {}, - FeatureCloudNativeArchival: {}, - FeatureCloudNativeProtection: {}, - FeatureExocompute: {}, - FeatureGCPSharedVPCHost: {}, - FeatureRDSProtection: {}, + FeatureAll: {}, + FeatureAppFlows: {}, + FeatureArchival: {}, + FeatureAzureSQLDBProtection: {}, + FeatureAzureSQLMIProtection: {}, + FeatureCloudAccounts: {}, + FeatureCloudNativeArchival: {}, + FeatureCloudNativeBLOBProtection: {}, + FeatureCloudNativeProtection: {}, + FeatureCloudNativeS3Protection: {}, + FeatureExocompute: {}, + FeatureGCPSharedVPCHost: {}, + FeatureKubernetesProtection: {}, + FeatureRDSProtection: {}, + FeatureServerAndApps: {}, +} + +// ContainsFeature returns true if the features slice contains the specified +// feature. +func ContainsFeature(features []Feature, feature Feature) bool { + for _, f := range features { + if f == feature { + return true + } + } + + return false } // FormatFeature returns the Feature as a string using lower case and with @@ -254,3 +277,26 @@ func (a API) DeploymentVersion(ctx context.Context) (string, error) { return payload.Data.DeploymentVersion, nil } + +// AllEnabledFeaturesForAccount returns all features enable for the RSC account. +func (a API) AllEnabledFeaturesForAccount(ctx context.Context) ([]Feature, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, allEnabledFeaturesForAccountQuery, struct{}{}) + if err != nil { + return nil, fmt.Errorf("failed to request allEnabledFeaturesForAccount: %w", err) + } + + var payload struct { + Data struct { + Result struct { + Features []Feature `json:"features"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal allEnabledFeaturesForAccount: %v", err) + } + + return payload.Data.Result.Features, nil +} diff --git a/pkg/polaris/graphql/core/queries.go b/pkg/polaris/graphql/core/queries.go index c90a7589..4571f059 100644 --- a/pkg/polaris/graphql/core/queries.go +++ b/pkg/polaris/graphql/core/queries.go @@ -24,6 +24,13 @@ package core +// allEnabledFeaturesForAccount GraphQL query +var allEnabledFeaturesForAccountQuery = `query SdkGolangAllEnabledFeaturesForAccount { + result: allEnabledFeaturesForAccount { + features + } +}` + // deploymentVersion GraphQL query var deploymentVersionQuery = `query SdkGolangDeploymentVersion { deploymentVersion diff --git a/pkg/polaris/graphql/core/queries/all_enabled_features_for_account.graphql b/pkg/polaris/graphql/core/queries/all_enabled_features_for_account.graphql new file mode 100644 index 00000000..724bcc4c --- /dev/null +++ b/pkg/polaris/graphql/core/queries/all_enabled_features_for_account.graphql @@ -0,0 +1,5 @@ +query RubrikPolarisSDKRequest { + result: allEnabledFeaturesForAccount { + features + } +} From 1e40fed5e745e2919454ac49d5592ec70058883e Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Mon, 18 Sep 2023 14:28:48 +0200 Subject: [PATCH 2/6] Fix static check warning --- pkg/polaris/aws/aws.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 33b76995..08e2c2a3 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -718,9 +718,7 @@ func (a API) TrustPolicies(ctx context.Context, id IdentityFunc, features []core if msg := artifact.ErrorMessage; msg != "" { return nil, fmt.Errorf("failed to get trust policies: %s", msg) } - if strings.HasSuffix(artifact.ExternalArtifactKey, roleArnSuffix) { - artifact.ExternalArtifactKey = strings.TrimSuffix(artifact.ExternalArtifactKey, roleArnSuffix) - } + artifact.ExternalArtifactKey = strings.TrimSuffix(artifact.ExternalArtifactKey, roleArnSuffix) trustPolicies[artifact.ExternalArtifactKey] = artifact.TrustPolicyDoc } From 430789e2e3e050575c13bff994e3de71255bd249 Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Mon, 18 Sep 2023 18:55:24 +0200 Subject: [PATCH 3/6] Add support for renaming AWS accounts --- pkg/polaris/aws/aws.go | 24 +++++++----- pkg/polaris/graphql/aws/cloud.go | 33 +++++++++++++++-- pkg/polaris/graphql/aws/cloud_no_cft.go | 37 +++++++++++++++++++ pkg/polaris/graphql/aws/queries.go | 20 +++++++++- .../queries/aws_artifacts_to_delete.graphql | 11 ++++++ .../queries/update_aws_cloud_account.graphql | 6 +-- .../update_aws_cloud_account_feature.graphql | 5 +++ 7 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 pkg/polaris/graphql/aws/queries/aws_artifacts_to_delete.graphql create mode 100644 pkg/polaris/graphql/aws/queries/update_aws_cloud_account_feature.graphql diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 08e2c2a3..98d5c6d7 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -547,32 +547,36 @@ func (a API) disableNativeAccount(ctx context.Context, id uuid.UUID, protectionF return nil } -// UpdateAccount updates the account with the specified id and feature. It's -// currently not possible to update the account name. +// UpdateAccount updates the account with the specified id and feature. Note +// that 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) var options options for _, option := range opts { if err := option(ctx, &options); err != nil { - return fmt.Errorf("failed to lookup option: %v", err) + return fmt.Errorf("failed to lookup option: %s", err) } } - if len(options.regions) == 0 { - return errors.New("nothing to update") - } account, err := a.Account(ctx, id, feature) if errors.Is(err, graphql.ErrNotFound) { return fmt.Errorf("failed to get account: %w", err) } if err != nil { - return fmt.Errorf("failed to get account: %v", err) + return fmt.Errorf("failed to get account: %s", err) } - err = aws.Wrap(a.client).UpdateCloudAccountFeature(ctx, core.UpdateRegions, account.ID, feature, options.regions) - if err != nil { - return fmt.Errorf("failed to update account: %v", err) + if options.name != "" { + if err := aws.Wrap(a.client).UpdateCloudAccount(ctx, account.ID, options.name); err != nil { + return fmt.Errorf("failed to update account: %s", err) + } + } + + if len(options.regions) > 0 { + if err := aws.Wrap(a.client).UpdateCloudAccountFeature(ctx, core.UpdateRegions, account.ID, feature, options.regions); err != nil { + return fmt.Errorf("failed to update account: %s", err) + } } return nil diff --git a/pkg/polaris/graphql/aws/cloud.go b/pkg/polaris/graphql/aws/cloud.go index b3e79ab7..2e215d6c 100644 --- a/pkg/polaris/graphql/aws/cloud.go +++ b/pkg/polaris/graphql/aws/cloud.go @@ -292,22 +292,47 @@ func (a API) FinalizeCloudAccountDeletion(ctx context.Context, id uuid.UUID, fea return nil } +// UpdateCloudAccount updates the name of the cloud account. +func (a API) UpdateCloudAccount(ctx context.Context, id uuid.UUID, accountName string) error { + a.GQL.Log().Print(log.Trace) + + buf, err := a.GQL.Request(ctx, updateAwsCloudAccountQuery, struct { + ID uuid.UUID `json:"cloudAccountId"` + AccountName string `json:"awsAccountName"` + }{ID: id, AccountName: accountName}) + if err != nil { + return fmt.Errorf("failed to request updateAwsCloudAccount: %w", err) + } + a.log.Printf(log.Debug, "updateAwsCloudAccount(%q, %q): %s", id, accountName, string(buf)) + + var payload struct { + Data struct { + Result struct{} `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return fmt.Errorf("failed to unmarshal updateAwsCloudAccount: %v", err) + } + + return nil +} + // UpdateCloudAccountFeature updates the settings of the cloud account. The // message returned by the GraphQL API call is converted into a Go error. At // this time only the regions can be updated. func (a API) UpdateCloudAccountFeature(ctx context.Context, action core.CloudAccountAction, id uuid.UUID, feature core.Feature, regions []Region) error { a.GQL.Log().Print(log.Trace) - buf, err := a.GQL.Request(ctx, updateAwsCloudAccountQuery, struct { + buf, err := a.GQL.Request(ctx, updateAwsCloudAccountFeatureQuery, struct { Action core.CloudAccountAction `json:"action"` ID uuid.UUID `json:"cloudAccountId"` Regions []Region `json:"awsRegions"` Feature core.Feature `json:"feature"` }{Action: action, ID: id, Regions: regions, Feature: feature}) if err != nil { - return fmt.Errorf("failed to request updateAwsCloudAccount: %w", err) + return fmt.Errorf("failed to request updateAwsCloudAccountFeature: %w", err) } - a.log.Printf(log.Debug, "updateAwsCloudAccount(%q, %q, %q, %q): %s", action, id, regions, feature, string(buf)) + a.log.Printf(log.Debug, "updateAwsCloudAccountFeature(%q, %q, %q, %q): %s", action, id, regions, feature, string(buf)) var payload struct { Data struct { @@ -317,7 +342,7 @@ func (a API) UpdateCloudAccountFeature(ctx context.Context, action core.CloudAcc } `json:"data"` } if err := json.Unmarshal(buf, &payload); err != nil { - return fmt.Errorf("failed to unmarshal updateAwsCloudAccount: %v", err) + return fmt.Errorf("failed to unmarshal updateAwsCloudAccountFeature: %v", err) } // On success the message starts with "successfully". diff --git a/pkg/polaris/graphql/aws/cloud_no_cft.go b/pkg/polaris/graphql/aws/cloud_no_cft.go index 3954781b..9886650e 100644 --- a/pkg/polaris/graphql/aws/cloud_no_cft.go +++ b/pkg/polaris/graphql/aws/cloud_no_cft.go @@ -54,6 +54,7 @@ func (a API) AllPermissionPolicies(ctx context.Context, cloud Cloud, features [] if err != nil { return nil, fmt.Errorf("failed to request allAwsPermissionPolicies: %w", err) } + a.log.Printf(log.Debug, "allAwsPermissionPolicies(%q, %v, %q): %s", cloud, features, ec2RecoveryRolePath, string(buf)) var payload struct { Data struct { @@ -100,6 +101,7 @@ func (a API) TrustPolicy(ctx context.Context, cloud Cloud, features []core.Featu if err != nil { return nil, fmt.Errorf("failed to request awsTrustPolicy: %w", err) } + a.log.Printf(log.Debug, "awsTrustPolicy(%q, %v, %v): %s", cloud, features, trustPolicyAccounts, string(buf)) var payload struct { Data struct { @@ -149,6 +151,7 @@ func (a API) RegisterFeatureArtifacts(ctx context.Context, cloud Cloud, artifact if err != nil { return nil, fmt.Errorf("failed to request registerAwsFeatureArtifacts: %w", err) } + a.log.Printf(log.Debug, "registerAwsFeatureArtifacts(%q, %v): %s", cloud, artifacts, string(buf)) var payload struct { Data struct { @@ -183,6 +186,7 @@ func (a API) DeleteCloudAccountWithoutCft(ctx context.Context, nativeID string, if err != nil { return nil, fmt.Errorf("failed to request bulkDeleteAwsCloudAccountWithoutCft: %w", err) } + a.log.Printf(log.Debug, "bulkDeleteAwsCloudAccountWithoutCft(%q, %v): %s", nativeID, features, string(buf)) var payload struct { Data struct { @@ -197,3 +201,36 @@ func (a API) DeleteCloudAccountWithoutCft(ctx context.Context, nativeID string, return payload.Data.Result.FeatureResult, nil } + +// ArtifactsToDelete holds the feature and the artifacts to delete. +type ArtifactsToDelete struct { + Feature string `json:"feature"` + ArtifactsToDelete []ExternalArtifact `json:"artifactsToDelete"` +} + +// ArtifactsToDelete returns the artifacts to delete. +func (a API) ArtifactsToDelete(ctx context.Context, nativeID string, features []core.Feature) ([]ArtifactsToDelete, error) { + a.log.Print(log.Trace) + + buf, err := a.GQL.Request(ctx, awsArtifactsToDeleteQuery, struct { + NativeID string `json:"awsNativeId"` + Features []core.Feature `json:"features"` + }{NativeID: nativeID, Features: features}) + if err != nil { + return nil, fmt.Errorf("failed to request awsArtifactsToDelete: %w", err) + } + a.log.Printf(log.Debug, "awsArtifactsToDelete(%q, %v): %s", nativeID, features, string(buf)) + + var payload struct { + Data struct { + Result struct { + ArtifactsToDelete []ArtifactsToDelete `json:"artifactsToDelete"` + } `json:"result"` + } `json:"data"` + } + if err := json.Unmarshal(buf, &payload); err != nil { + return nil, fmt.Errorf("failed to unmarshal awsArtifactsToDelete: %v", err) + } + + return payload.Data.Result.ArtifactsToDelete, nil +} diff --git a/pkg/polaris/graphql/aws/queries.go b/pkg/polaris/graphql/aws/queries.go index 61a2372a..43af07cf 100644 --- a/pkg/polaris/graphql/aws/queries.go +++ b/pkg/polaris/graphql/aws/queries.go @@ -118,6 +118,19 @@ var allVpcsByRegionFromAwsQuery = `query SdkGolangAllVpcsByRegionFromAws($awsAcc } }` +// awsArtifactsToDelete GraphQL query +var awsArtifactsToDeleteQuery = `query SdkGolangAwsArtifactsToDelete($awsNativeId: String!, $features: [CloudAccountFeature!]!) { + result: awsArtifactsToDelete(input: {awsNativeId: $awsNativeId, features: $features}) { + artifactsToDelete { + feature + artifactsToDelete { + externalArtifactKey + externalArtifactValue + } + } + } +}` + // awsCloudAccountWithFeatures GraphQL query var awsCloudAccountWithFeaturesQuery = `query SdkGolangAwsCloudAccountWithFeatures($cloudAccountId: UUID!, $features: [CloudAccountFeature!]!) { result: awsCloudAccountWithFeatures(cloudAccountId: $cloudAccountId, awsCloudAccountArg: {features: $features}) { @@ -325,7 +338,12 @@ var startAwsNativeAccountDisableJobQuery = `mutation SdkGolangStartAwsNativeAcco }` // updateAwsCloudAccount GraphQL query -var updateAwsCloudAccountQuery = `mutation SdkGolangUpdateAwsCloudAccount($action: CloudAccountAction!, $cloudAccountId: UUID!, $awsRegions: [AwsCloudAccountRegion!]!, $feature: CloudAccountFeature!) { +var updateAwsCloudAccountQuery = `mutation SdkGolangUpdateAwsCloudAccount($cloudAccountId: UUID!, $awsAccountName: String) { + result: updateAwsCloudAccount(input: {cloudAccountId: $cloudAccountId, awsAccountName: $awsAccountName}) +}` + +// updateAwsCloudAccountFeature GraphQL query +var updateAwsCloudAccountFeatureQuery = `mutation SdkGolangUpdateAwsCloudAccountFeature($action: CloudAccountAction!, $cloudAccountId: UUID!, $awsRegions: [AwsCloudAccountRegion!]!, $feature: CloudAccountFeature!) { result: updateAwsCloudAccountFeature(input: {action: $action, cloudAccountId: $cloudAccountId, awsRegions: $awsRegions, feature: $feature}) { message } diff --git a/pkg/polaris/graphql/aws/queries/aws_artifacts_to_delete.graphql b/pkg/polaris/graphql/aws/queries/aws_artifacts_to_delete.graphql new file mode 100644 index 00000000..51767eb1 --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/aws_artifacts_to_delete.graphql @@ -0,0 +1,11 @@ +query RubrikPolarisSDKRequest($awsNativeId: String!, $features: [CloudAccountFeature!]!) { + result: awsArtifactsToDelete(input: {awsNativeId: $awsNativeId, features: $features}) { + artifactsToDelete { + feature + artifactsToDelete { + externalArtifactKey + externalArtifactValue + } + } + } +} diff --git a/pkg/polaris/graphql/aws/queries/update_aws_cloud_account.graphql b/pkg/polaris/graphql/aws/queries/update_aws_cloud_account.graphql index f1ecfa21..602e50a3 100644 --- a/pkg/polaris/graphql/aws/queries/update_aws_cloud_account.graphql +++ b/pkg/polaris/graphql/aws/queries/update_aws_cloud_account.graphql @@ -1,5 +1,3 @@ -mutation RubrikPolarisSDKRequest($action: CloudAccountAction!, $cloudAccountId: UUID!, $awsRegions: [AwsCloudAccountRegion!]!, $feature: CloudAccountFeature!) { - result: updateAwsCloudAccountFeature(input: {action: $action, cloudAccountId: $cloudAccountId, awsRegions: $awsRegions, feature: $feature}) { - message - } +mutation RubrikPolarisSDKRequest($cloudAccountId: UUID!, $awsAccountName: String) { + result: updateAwsCloudAccount(input: {cloudAccountId: $cloudAccountId, awsAccountName: $awsAccountName}) } diff --git a/pkg/polaris/graphql/aws/queries/update_aws_cloud_account_feature.graphql b/pkg/polaris/graphql/aws/queries/update_aws_cloud_account_feature.graphql new file mode 100644 index 00000000..f1ecfa21 --- /dev/null +++ b/pkg/polaris/graphql/aws/queries/update_aws_cloud_account_feature.graphql @@ -0,0 +1,5 @@ +mutation RubrikPolarisSDKRequest($action: CloudAccountAction!, $cloudAccountId: UUID!, $awsRegions: [AwsCloudAccountRegion!]!, $feature: CloudAccountFeature!) { + result: updateAwsCloudAccountFeature(input: {action: $action, cloudAccountId: $cloudAccountId, awsRegions: $awsRegions, feature: $feature}) { + message + } +} From 3f45125edfb86038ab2d46bed8087f4bb44c3fa3 Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Tue, 19 Sep 2023 09:29:45 +0200 Subject: [PATCH 4/6] Add AccountArtifacts function --- pkg/polaris/aws/aws.go | 63 ++++++++++++++++--------- pkg/polaris/graphql/aws/cloud_no_cft.go | 9 ++-- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 98d5c6d7..46575bfe 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -559,22 +559,19 @@ func (a API) UpdateAccount(ctx context.Context, id IdentityFunc, feature core.Fe } } - account, err := a.Account(ctx, id, feature) - if errors.Is(err, graphql.ErrNotFound) { - return fmt.Errorf("failed to get account: %w", err) - } + accountID, err := a.toCloudAccountID(ctx, id) if err != nil { - return fmt.Errorf("failed to get account: %s", err) + return err } if options.name != "" { - if err := aws.Wrap(a.client).UpdateCloudAccount(ctx, account.ID, options.name); err != nil { + if err := aws.Wrap(a.client).UpdateCloudAccount(ctx, accountID, options.name); err != nil { return fmt.Errorf("failed to update account: %s", err) } } if len(options.regions) > 0 { - if err := aws.Wrap(a.client).UpdateCloudAccountFeature(ctx, core.UpdateRegions, account.ID, feature, options.regions); err != nil { + if err := aws.Wrap(a.client).UpdateCloudAccountFeature(ctx, core.UpdateRegions, accountID, feature, options.regions); err != nil { return fmt.Errorf("failed to update account: %s", err) } } @@ -623,20 +620,50 @@ func (a API) Artifacts(ctx context.Context, cloud string, features []core.Featur return profiles, roles, nil } +// AccountArtifacts returns the artifacts added to the cloud account. +func (a API) AccountArtifacts(ctx context.Context, id IdentityFunc) (map[string]string, map[string]string, error) { + a.log.Print(log.Trace) + + nativeID, err := a.toNativeID(ctx, id) + if err != nil { + return nil, nil, err + } + + artifacts, err := aws.Wrap(a.client).ArtifactsToDelete(ctx, nativeID) + if err != nil { + return nil, nil, fmt.Errorf("%s", err) + } + + instanceProfiles := make(map[string]string) + roles := make(map[string]string) + var skipped []string + for _, artifact := range artifacts { + for _, artifact := range artifact.ArtifactsToDelete { + switch { + case strings.HasSuffix(artifact.ExternalArtifactKey, instanceProfileSuffix): + key := strings.TrimSuffix(artifact.ExternalArtifactKey, instanceProfileSuffix) + instanceProfiles[key] = artifact.ExternalArtifactValue + case strings.HasSuffix(artifact.ExternalArtifactKey, roleArnSuffix): + key := strings.TrimSuffix(artifact.ExternalArtifactKey, roleArnSuffix) + roles[key] = artifact.ExternalArtifactValue + default: + skipped = append(skipped, artifact.ExternalArtifactKey) + } + } + } + a.log.Printf(log.Debug, "Skipped the following artifacts: %v", skipped) + + return instanceProfiles, roles, nil +} + // AddAccountArtifacts adds the specified artifacts, instance profiles and // roles, to the cloud account. func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features []core.Feature, instanceProfiles map[string]string, roles map[string]string) (uuid.UUID, error) { a.log.Print(log.Trace) - if id == nil { - return uuid.Nil, errors.New("id is not allowed to be nil") - } account, err := a.Account(ctx, id, core.FeatureAll) - if errors.Is(err, graphql.ErrNotFound) { - return uuid.Nil, fmt.Errorf("failed to get account: %w", err) - } if err != nil { - return uuid.Nil, fmt.Errorf("failed to get account: %s", err) + return uuid.Nil, err } externalArtifacts := make([]aws.ExternalArtifact, 0, len(instanceProfiles)+len(roles)) @@ -695,15 +722,9 @@ func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features func (a API) TrustPolicies(ctx context.Context, id IdentityFunc, features []core.Feature, externalID string) (map[string]string, error) { a.log.Print(log.Trace) - if id == nil { - return nil, errors.New("id is not allowed to be nil") - } account, err := a.Account(ctx, id, core.FeatureAll) - if errors.Is(err, graphql.ErrNotFound) { - return nil, fmt.Errorf("failed to get account: %w", err) - } if err != nil { - return nil, fmt.Errorf("failed to get account: %s", err) + return nil, err } policies, err := aws.Wrap(a.client).TrustPolicy(ctx, aws.Cloud(account.Cloud), features, []aws.TrustPolicyAccount{{ diff --git a/pkg/polaris/graphql/aws/cloud_no_cft.go b/pkg/polaris/graphql/aws/cloud_no_cft.go index 9886650e..3ee8650f 100644 --- a/pkg/polaris/graphql/aws/cloud_no_cft.go +++ b/pkg/polaris/graphql/aws/cloud_no_cft.go @@ -208,18 +208,19 @@ type ArtifactsToDelete struct { ArtifactsToDelete []ExternalArtifact `json:"artifactsToDelete"` } -// ArtifactsToDelete returns the artifacts to delete. -func (a API) ArtifactsToDelete(ctx context.Context, nativeID string, features []core.Feature) ([]ArtifactsToDelete, error) { +// ArtifactsToDelete returns all feature artifacts registered with the cloud +// account. +func (a API) ArtifactsToDelete(ctx context.Context, nativeID string) ([]ArtifactsToDelete, error) { a.log.Print(log.Trace) buf, err := a.GQL.Request(ctx, awsArtifactsToDeleteQuery, struct { NativeID string `json:"awsNativeId"` Features []core.Feature `json:"features"` - }{NativeID: nativeID, Features: features}) + }{NativeID: nativeID, Features: []core.Feature{}}) if err != nil { return nil, fmt.Errorf("failed to request awsArtifactsToDelete: %w", err) } - a.log.Printf(log.Debug, "awsArtifactsToDelete(%q, %v): %s", nativeID, features, string(buf)) + a.log.Printf(log.Debug, "awsArtifactsToDelete(%q): %s", nativeID, string(buf)) var payload struct { Data struct { From d181fb41df53d94307f77bdcdae7e27310f87dd8 Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Wed, 20 Sep 2023 11:24:25 +0200 Subject: [PATCH 5/6] Worked in code review findings --- pkg/polaris/aws/aws.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 46575bfe..17aa74f4 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -40,10 +40,6 @@ import ( "github.com/rubrikinc/rubrik-polaris-sdk-for-go/pkg/polaris/log" ) -const ( - requestTimeout = 5 * time.Second -) - // API for AWS account management. type API struct { client *graphql.Client @@ -686,8 +682,13 @@ func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features }) } + // RegisterFeatureArtifacts fails with an error referring to RBK30300003 + // if a role passed in as an artifact is not yet available. This can happen + // if the call to register the artifacts is performed right after the call + // to create the role returns. When this happens we wait 5 seconds before + // trying again. After 30 seconds we abort. var mappings []aws.NativeIDToRSCIDMapping - for i := 0; i < 7; i++ { + for i := 0; true; i++ { mappings, err = aws.Wrap(a.client).RegisterFeatureArtifacts(ctx, aws.Cloud(account.Cloud), []aws.AccountFeatureArtifact{{ NativeID: account.NativeID, Features: features, @@ -702,8 +703,10 @@ func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features if msg := mappings[0].Message; msg == "" || !strings.Contains(msg, "RBK30300003") { break } - - time.Sleep(requestTimeout) + if i > 5 { + break + } + time.Sleep(5 * time.Second) } if msg := mappings[0].Message; msg != "" { return uuid.Nil, fmt.Errorf("failed to register feature artifacts: %s", msg) From b7df059d2b152e7381849b2b6f5285dfe79b8234 Mon Sep 17 00:00:00 2001 From: Johan Andersson Date: Wed, 20 Sep 2023 13:07:47 +0200 Subject: [PATCH 6/6] Work in code review findings --- pkg/polaris/aws/aws.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/polaris/aws/aws.go b/pkg/polaris/aws/aws.go index 17aa74f4..17dbab1f 100644 --- a/pkg/polaris/aws/aws.go +++ b/pkg/polaris/aws/aws.go @@ -683,12 +683,14 @@ func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features } // RegisterFeatureArtifacts fails with an error referring to RBK30300003 - // if a role passed in as an artifact is not yet available. This can happen - // if the call to register the artifacts is performed right after the call - // to create the role returns. When this happens we wait 5 seconds before - // trying again. After 30 seconds we abort. + // if an instance profile or a role passed in as an artifact is not yet + // available. This can happen if the call to register the artifacts is + // performed right after the call to create the instance profile or the role + // returns. When this happens we wait 5 seconds before trying again. After + // 30 seconds we abort. + now := time.Now() var mappings []aws.NativeIDToRSCIDMapping - for i := 0; true; i++ { + for { mappings, err = aws.Wrap(a.client).RegisterFeatureArtifacts(ctx, aws.Cloud(account.Cloud), []aws.AccountFeatureArtifact{{ NativeID: account.NativeID, Features: features, @@ -703,7 +705,7 @@ func (a API) AddAccountArtifacts(ctx context.Context, id IdentityFunc, features if msg := mappings[0].Message; msg == "" || !strings.Contains(msg, "RBK30300003") { break } - if i > 5 { + if time.Since(now) > 30*time.Second { break } time.Sleep(5 * time.Second)