Skip to content

Commit 82f9a7b

Browse files
authored
Merge pull request #1 from omattsson/add_rbac_retry
Add rbac retry
2 parents baf374e + 5ba33de commit 82f9a7b

File tree

8 files changed

+606
-1237
lines changed

8 files changed

+606
-1237
lines changed

azurehelper/azure_blob.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import (
55
"context"
66
"fmt"
77
"io"
8-
"net"
98
"net/http"
109
"strings"
11-
"time"
1210

1311
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1412
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
@@ -18,25 +16,6 @@ import (
1816
"github.com/gruntwork-io/terragrunt/pkg/log"
1917
)
2018

21-
// waitForStorageAccountDNS waits for the storage account DNS to propagate and become available.
22-
func waitForStorageAccountDNS(name string, maxWait time.Duration) error {
23-
url := name + ".blob.core.windows.net"
24-
deadline := time.Now().Add(maxWait)
25-
26-
for {
27-
_, err := net.LookupHost(url)
28-
if err == nil {
29-
return nil // DNS is ready
30-
}
31-
32-
if time.Now().After(deadline) {
33-
return fmt.Errorf("storage account DNS not available after %s: %w", maxWait, err)
34-
}
35-
36-
time.Sleep(3 * time.Second) //nolint: mnd
37-
}
38-
}
39-
4019
// BlobServiceClient wraps Azure's azblob client to provide a simpler interface for our needs.
4120
type BlobServiceClient struct {
4221
client *azblob.Client
@@ -88,40 +67,13 @@ func (e *AzureResponseError) Error() string {
8867
return fmt.Sprintf("Azure API error (StatusCode=%d, ErrorCode=%s): %s", e.StatusCode, e.ErrorCode, e.Message)
8968
}
9069

91-
// IsVersioningEnabled checks if versioning is enabled for a blob storage account.
92-
// In Azure Blob Storage, versioning is a storage account level setting and cannot be
93-
// configured at the container level. This method always returns true as versioning
94-
// state needs to be checked at the storage account level.
95-
func (c *BlobServiceClient) IsVersioningEnabled(ctx context.Context, containerName string) (bool, error) {
96-
l := log.Default()
97-
l.Warnf("Warning: Blob versioning in Azure Storage is a storage account level setting, not a container level setting")
98-
99-
return true, nil
100-
}
101-
102-
// EnableVersioningIfNecessary is deprecated as versioning is a storage account level setting.
103-
func (c *BlobServiceClient) EnableVersioningIfNecessary(ctx context.Context, l log.Logger, containerName string) error {
104-
l.Warnf("Warning: Blob versioning in Azure Storage is a storage account level setting and cannot be configured at container level")
105-
106-
return nil
107-
}
108-
10970
// CreateBlobServiceClient creates a new Azure Blob Service client using the configuration from the backend.
11071
func CreateBlobServiceClient(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, config map[string]interface{}) (*BlobServiceClient, error) {
11172
storageAccountName, okStorageAccountName := config["storage_account_name"].(string)
11273
if !okStorageAccountName || storageAccountName == "" {
11374
return nil, errors.Errorf("storage_account_name is required")
11475
}
11576

116-
// Wait for DNS propagation only if explicitly requested via config
117-
// This is useful when the storage account was just created and DNS might not be propagated yet
118-
if waitForDNS, ok := config["wait_for_dns"].(bool); ok && waitForDNS {
119-
l.Infof("Waiting for DNS propagation for storage account %s", storageAccountName)
120-
121-
if err := waitForStorageAccountDNS(storageAccountName, 60*time.Second); err != nil { //nolint: mnd
122-
l.Warnf("DNS propagation wait failed for %s: %v", storageAccountName, err)
123-
}
124-
}
12577
// Extract resource group and subscription ID if provided
12678
resourceGroupName, _ := config["resource_group_name"].(string)
12779
subscriptionID, _ := config["subscription_id"].(string)

azurehelper/azure_constants.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ const (
2828
AccessTierHot = "Hot"
2929
AccessTierCool = "Cool"
3030
AccessTierPremium = "Premium"
31+
32+
// RBAC propagation retry configuration
33+
RbacRetryDelay = 3 * time.Second
34+
RbacMaxRetries = 5
35+
RbacRetryAttempts = RbacMaxRetries + 1 // Total attempts including first try
3136
)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package azurehelper_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/gruntwork-io/terragrunt/azurehelper"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
// TestRBACRetryConstants ensures the RBAC retry constants have the expected values
14+
// and that the retry attempts is correctly calculated from max retries
15+
func TestRBACRetryConstants(t *testing.T) {
16+
t.Parallel()
17+
// Test RBAC delay is 3 seconds
18+
assert.Equal(t, 3*time.Second, azurehelper.RbacRetryDelay, "RBAC retry delay should be 3 seconds")
19+
20+
// Test RBAC max retries is 5
21+
assert.Equal(t, 5, azurehelper.RbacMaxRetries, "RBAC max retries should be 5")
22+
23+
// Test that retry attempts equals max retries + 1 (for the initial attempt)
24+
assert.Equal(t, azurehelper.RbacMaxRetries+1, azurehelper.RbacRetryAttempts,
25+
"RBAC retry attempts should equal RbacMaxRetries+1")
26+
27+
// Test the specific expected values
28+
assert.Equal(t, 6, azurehelper.RbacRetryAttempts, "RBAC retry attempts should be 6 (5 retries + initial attempt)")
29+
30+
// The constants are defined in the package, so we can't modify them for testing
31+
// Instead, we'll just verify the current values match our expectations
32+
33+
// Test different values of the relationship
34+
testCases := []struct {
35+
maxRetries int
36+
expectedAttempts int
37+
}{
38+
{3, 4},
39+
{5, 6}, // Current value
40+
{10, 11},
41+
{0, 1}, // Edge case: no retries means 1 attempt
42+
}
43+
44+
for _, tc := range testCases {
45+
calculatedAttempts := tc.maxRetries + 1
46+
assert.Equal(t, tc.expectedAttempts, calculatedAttempts,
47+
"When max retries is %d, attempts should be %d", tc.maxRetries, tc.expectedAttempts)
48+
}
49+
}
50+
51+
// TestIsPermissionError tests the isPermissionError function with various error types
52+
// nolint: govet
53+
func TestIsPermissionError(t *testing.T) {
54+
t.Parallel()
55+
// Create storage account client for testing
56+
client := &azurehelper.StorageAccountClient{}
57+
58+
// Test cases for various error messages
59+
testCases := []struct {
60+
name string
61+
input error
62+
expected bool
63+
}{
64+
{
65+
name: "nil error",
66+
input: nil,
67+
expected: false,
68+
},
69+
{
70+
name: "basic authorization error",
71+
input: errors.New("operation failed due to authorization failed"),
72+
expected: true,
73+
},
74+
{
75+
name: "permission denied error",
76+
input: errors.New("permission denied for operation"),
77+
expected: true,
78+
},
79+
{
80+
name: "forbidden error",
81+
input: errors.New("server returned status code 403 forbidden"),
82+
expected: true,
83+
},
84+
{
85+
name: "access denied error",
86+
input: errors.New("access denied to resource"),
87+
expected: true,
88+
},
89+
{
90+
name: "not authorized error",
91+
input: errors.New("client is not authorized to perform this action"),
92+
expected: true,
93+
},
94+
{
95+
name: "role assignment error",
96+
input: errors.New("waiting for role assignment to complete"),
97+
expected: true,
98+
},
99+
{
100+
name: "storage blob data owner error",
101+
input: errors.New("requires storage blob data owner role"),
102+
expected: true,
103+
},
104+
{
105+
name: "unrelated error",
106+
input: errors.New("connection timed out"),
107+
expected: false,
108+
},
109+
{
110+
name: "network error",
111+
input: errors.New("network connectivity issues"),
112+
expected: false,
113+
},
114+
{
115+
name: "validation error",
116+
input: errors.New("validation failed for resource"),
117+
expected: false,
118+
},
119+
}
120+
121+
// Run test cases
122+
for _, tc := range testCases {
123+
t.Run(tc.name, func(t *testing.T) {
124+
t.Parallel()
125+
result := client.IsPermissionError(tc.input)
126+
assert.Equal(t, tc.expected, result,
127+
"IsPermissionError should return %v for error: %v", tc.expected, tc.input)
128+
})
129+
}
130+
131+
// Test with wrapped errors
132+
t.Run("WrappedPermissionError", func(t *testing.T) {
133+
t.Parallel()
134+
innerErr := errors.New("permission denied for storage account")
135+
wrappedErr := fmt.Errorf("operation failed: %w", innerErr)
136+
137+
assert.True(t, client.IsPermissionError(wrappedErr),
138+
"Should detect permission error in wrapped error")
139+
})
140+
141+
// Test with non-permission wrapped errors
142+
t.Run("WrappedNonPermissionError", func(t *testing.T) {
143+
t.Parallel()
144+
innerErr := errors.New("resource not found")
145+
wrappedErr2 := fmt.Errorf("operation failed: %w", innerErr)
146+
147+
assert.False(t, client.IsPermissionError(wrappedErr2),
148+
"Should not detect permission error in unrelated wrapped error")
149+
})
150+
}

0 commit comments

Comments
 (0)