Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_key_vault: use Resource Graph API to search keyvault resource from keyvault name #23413

Closed
wants to merge 2 commits into from

Conversation

wuxu92
Copy link
Contributor

@wuxu92 wuxu92 commented Sep 28, 2023

Currently the resourceClient.List API is low performance and with high probability of rasing a context deadline exceeded if the subscription has over thousands of resources.

As there are several PRs try to migrate this logic to ARG before (like #13719 but closed at last), this PR is only use ARG as a high pri option and it will fall back to origin logic when ARG fails. And this PR is not to fix the issue of keyvault eventual consistant, but only to improve the performance of the KeyVaultIDFromBaseUrl function, Because this function is prone to causing the context deadline exceed error on the production environment of our customers.

To avoid breaking the KeyVaultIDFromBaseUrl signature, I placed the ResourceGraph Client under resourcesClient.Client. This way, we won't have to make changes to multiple files that depend on it.

This should be a long-term fix to #23404.

Copy link
Collaborator

@WodansSon WodansSon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @wuxu92, This is looking really good so far, however I have left a few comments and suggestions on the code. It LGTM more or less, we will have to see if @jackofallops and @tombuildsstuff agrees. 🚀

@@ -111,6 +113,32 @@ func (c *Client) Exists(ctx context.Context, keyVaultId commonids.KeyVaultId) (b
return true, nil
}

func (c *Client) KeyVaultIDFromBaseURLByGraph(ctx context.Context, client *graph.ResourcesClient, name string) (string, interface{}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the return signature for this function be just *string?

Suggested change
func (c *Client) KeyVaultIDFromBaseURLByGraph(ctx context.Context, client *graph.ResourcesClient, name string) (string, interface{}) {
func (c *Client) KeyVaultIDFromBaseURLByGraph(ctx context.Context, client *graph.ResourcesClient, name string) *string {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (string, error) should be fine. I made a mistake here to define the second value as interface{}.

@@ -111,6 +113,32 @@ func (c *Client) Exists(ctx context.Context, keyVaultId commonids.KeyVaultId) (b
return true, nil
}

func (c *Client) KeyVaultIDFromBaseURLByGraph(ctx context.Context, client *graph.ResourcesClient, name string) (string, interface{}) {
if name == "" {
return "", nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", nil
return nil

}
resp, err := client.Resources(ctx, req)
if err != nil {
return "", err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", err
return nil

}
}
}
return "", fmt.Errorf("no such keyvault in resource graph by query: %s", req.Query)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", fmt.Errorf("no such keyvault in resource graph by query: %s", req.Query)
return nil

@@ -130,6 +158,15 @@ func (c *Client) KeyVaultIDFromBaseUrl(ctx context.Context, resourcesClient *res
return &v.keyVaultId, nil
}

// try to get the keyVault id by resource graph API first to void the expensive ResourcesClient.List operation
// if it cannot fetch the keyvault from resource graph API, it will fall back to the resourceClient.List method
if idStr, err := c.KeyVaultIDFromBaseURLByGraph(ctx, resourcesClient.GraphResourceClient, *keyVaultName); err == nil && idStr != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the changes above idStr will always return nil if it was not successful for any reason. Should we only check for idStr not being nil here? Makes it a little easier to read maybe?

Suggested change
if idStr, err := c.KeyVaultIDFromBaseURLByGraph(ctx, resourcesClient.GraphResourceClient, *keyVaultName); err == nil && idStr != "" {
if idStr := c.KeyVaultIDFromBaseURLByGraph(ctx, resourcesClient.GraphResourceClient, *keyVaultName); idStr != nil {

// try to get the keyVault id by resource graph API first to void the expensive ResourcesClient.List operation
// if it cannot fetch the keyvault from resource graph API, it will fall back to the resourceClient.List method
if idStr, err := c.KeyVaultIDFromBaseURLByGraph(ctx, resourcesClient.GraphResourceClient, *keyVaultName); err == nil && idStr != "" {
if id, err := commonids.ParseKeyVaultID(idStr); err == nil && strings.EqualFold(id.VaultName, *keyVaultName) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if id, err := commonids.ParseKeyVaultID(idStr); err == nil && strings.EqualFold(id.VaultName, *keyVaultName) {
if id, err := commonids.ParseKeyVaultID(pointer.From(idStr)); err == nil && strings.EqualFold(id.VaultName, pointer.From(keyVaultName)) {

// if it cannot fetch the keyvault from resource graph API, it will fall back to the resourceClient.List method
if idStr, err := c.KeyVaultIDFromBaseURLByGraph(ctx, resourcesClient.GraphResourceClient, *keyVaultName); err == nil && idStr != "" {
if id, err := commonids.ParseKeyVaultID(idStr); err == nil && strings.EqualFold(id.VaultName, *keyVaultName) {
c.AddToCache(*id, keyVaultBaseUrl)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
c.AddToCache(*id, keyVaultBaseUrl)
c.AddToCache(pointer.From(id), keyVaultBaseUrl)

}

if resp.Model != nil && resp.Model.Data != nil {
if list, ok := resp.Model.Data.([]interface{}); ok && len(list) == 1 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it returns 0 or 2? Is returning the no such keyvault in resource graph by query the correct behavior here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shoud be 1 or 0 resource for keyvault resource of the same name.

if res, ok := list[0].(map[string]interface{}); ok {
if id, ok := res["id"]; ok {
if idStr, ok := id.(string); ok {
return idStr, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return idStr, nil
return pointer.To(idStr)

@tombuildsstuff
Copy link
Contributor

hey @wuxu92

Thanks for this PR - apologies for the delayed review here.

Whilst we've investigated the ResourceGraph API in the past we've noticed that it contains similar caching issues to the Resources API - and as such we believe that switching to load data from this API won't fundamentally resolve this issue.

That said, as a part of reviewing both the background for this PR, this PR and reviewing what's left to migrate off of Azure/azure-sdk-for-go more broadly - I've taken a closer look into the Resources API usage across the Provider and tried to determine if there's an alternate solution we can use here.

As a part of that I've reviewed the caching setup and realised that we can take a different approach with this one, rather than loading the Key Vaults on demand when populating the cache (one at a time) - since we're hitting the Azure API to get the information for the specified Key Vault - we might as well pull the entire list into the cache at the same time prior to filtering the cache for the Key Vault in question.

This means that we can use the more up to date ListBySubscriptions API within the Microsoft.KeyVault Resource Provider - which (whilst from memory) does have a lower rate limit, since we're now populating all of the Key Vaults within the Subscription - should be fine/resolve this issue. It's worth noting that this matches the approach used for Storage Accounts - but additional information can be found in this issue.

As such whilst I'd like to thank you for this contribution I'm going to close this PR for the moment, as I believe that #24019 will solve #23404 (and the other issues) albeit in a different manner.

Thanks!

Copy link

github-actions bot commented May 6, 2024

I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 6, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants