Skip to content

Commit 264681e

Browse files
mgmt, retry on 404 in service principal create code, for eventual consistency (Azure#30791)
1 parent 9cb7512 commit 264681e

File tree

24 files changed

+5042
-8686
lines changed

24 files changed

+5042
-8686
lines changed

sdk/resourcemanager/azure-resourcemanager-appservice/src/test/java/com/azure/resourcemanager/appservice/WebAppsMsiTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public void canCRUDWebAppWithMsi() throws Exception {
8383

8484
ResourceManagerUtils.sleep(Duration.ofMinutes(1));
8585

86-
Response<String> response = curl("http://" + webappName1 + "." + "azurewebsites.net/appservicemsi/");
86+
Response<String> response = curl("https://" + webappName1 + "." + "azurewebsites.net/appservicemsi/");
8787
Assertions.assertEquals(200, response.getStatusCode());
8888
String body = response.getValue();
8989
Assertions.assertNotNull(body);
@@ -173,9 +173,9 @@ public void canCRUDWebAppWithUserAssignedMsi() throws Exception {
173173
"appservicemsi.war",
174174
WebAppsMsiTests.class.getResourceAsStream("/appservicemsi.war"));
175175

176-
ResourceManagerUtils.sleep(Duration.ofSeconds(30));
176+
ResourceManagerUtils.sleep(Duration.ofMinutes(1));
177177

178-
Response<String> response = curl("http://" + webappName1 + "." + "azurewebsites.net/appservicemsi/");
178+
Response<String> response = curl("https://" + webappName1 + "." + "azurewebsites.net/appservicemsi/");
179179
Assertions.assertEquals(200, response.getStatusCode());
180180
String body = response.getValue();
181181
Assertions.assertNotNull(body);

sdk/resourcemanager/azure-resourcemanager-appservice/src/test/resources/session-records/WebAppsMsiTests.canCRUDWebAppWithMsi.json

Lines changed: 144 additions & 198 deletions
Large diffs are not rendered by default.

sdk/resourcemanager/azure-resourcemanager-appservice/src/test/resources/session-records/WebAppsMsiTests.canCRUDWebAppWithUserAssignedMsi.json

Lines changed: 218 additions & 407 deletions
Large diffs are not rendered by default.

sdk/resourcemanager/azure-resourcemanager-authorization/CHANGELOG.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22

33
## 2.19.0-beta.1 (Unreleased)
44

5-
### Features Added
6-
7-
### Breaking Changes
8-
95
### Bugs Fixed
106

11-
### Other Changes
7+
- Supported delayed retry on 404 for eventual consistency, after creating AAD service principal.
8+
- Improved the delayed retry on 400 for service principal, when creating role assignment. Now the retry will continue for only about a minute.
129

1310
## 2.18.0 (2022-08-26)
1411

sdk/resourcemanager/azure-resourcemanager-authorization/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
<javaModulesSurefireArgLine>
4444
--add-opens com.azure.resourcemanager.authorization/com.azure.resourcemanager.authorization=ALL-UNNAMED
4545
--add-opens com.azure.resourcemanager.resources/com.azure.resourcemanager.resources=ALL-UNNAMED
46+
47+
--add-opens com.azure.core/com.azure.core.implementation.util=ALL-UNNAMED
4648
</javaModulesSurefireArgLine>
4749
</properties>
4850

sdk/resourcemanager/azure-resourcemanager-authorization/src/main/java/com/azure/resourcemanager/authorization/implementation/ActiveDirectoryApplicationImpl.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package com.azure.resourcemanager.authorization.implementation;
55

6-
import com.azure.core.exception.HttpResponseException;
76
import com.azure.resourcemanager.authorization.AuthorizationManager;
87
import com.azure.resourcemanager.authorization.fluent.models.ApplicationsAddPasswordRequestBodyInner;
98
import com.azure.resourcemanager.authorization.fluent.models.MicrosoftGraphApplicationInner;
@@ -14,15 +13,12 @@
1413
import com.azure.resourcemanager.authorization.models.CertificateCredential;
1514
import com.azure.resourcemanager.authorization.models.PasswordCredential;
1615
import com.azure.resourcemanager.resources.fluentcore.model.implementation.CreatableUpdatableImpl;
17-
import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils;
1816
import reactor.core.publisher.Flux;
1917
import reactor.core.publisher.Mono;
2018
import reactor.util.retry.Retry;
21-
import reactor.util.retry.RetryBackoffSpec;
2219

2320
import java.net.MalformedURLException;
2421
import java.net.URL;
25-
import java.time.Duration;
2622
import java.util.ArrayList;
2723
import java.util.Collections;
2824
import java.util.HashMap;
@@ -65,7 +61,7 @@ public boolean isInCreateMode() {
6561

6662
@Override
6763
public Mono<ActiveDirectoryApplication> createResourceAsync() {
68-
Retry retry = backoffRetryFor404();
64+
Retry retry = RetryUtils.backoffRetryFor404ResourceNotFound();
6965

7066
return manager
7167
.serviceClient()
@@ -82,15 +78,6 @@ public Mono<ActiveDirectoryApplication> updateResourceAsync() {
8278
.then(submitCredentialAsync(null).doOnComplete(this::postRequest).then(refreshAsync()));
8379
}
8480

85-
static RetryBackoffSpec backoffRetryFor404() {
86-
return Retry
87-
// 10 + 20 + 40 = 70 seconds
88-
.backoff(3, ResourceManagerUtils.InternalRuntimeContext.getDelayDuration(Duration.ofSeconds(10)))
89-
.filter(e -> (e instanceof HttpResponseException)
90-
&& (((HttpResponseException) e).getResponse().getStatusCode() == 404))
91-
// do not convert to RetryExhaustedException
92-
.onRetryExhaustedThrow((spec, signal) -> signal.failure());
93-
}
9481

9582
private void refreshCredentials(MicrosoftGraphApplicationInner inner) {
9683
cachedCertificateCredentials.clear();
@@ -118,7 +105,6 @@ private Flux<?> submitCredentialAsync(Retry retry) {
118105
manager().serviceClient().getApplications()
119106
.addPasswordAsync(id(), new ApplicationsAddPasswordRequestBodyInner()
120107
.withPasswordCredential(passwordCredential.innerModel()));
121-
122108
if (retry != null) {
123109
monoAddPassword = monoAddPassword.retryWhen(retry);
124110
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.resourcemanager.authorization.implementation;
5+
6+
import com.azure.core.management.exception.ManagementException;
7+
import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils;
8+
import reactor.util.retry.Retry;
9+
import reactor.util.retry.RetryBackoffSpec;
10+
11+
import java.time.Duration;
12+
13+
class RetryUtils {
14+
15+
// for MSGraph API
16+
static RetryBackoffSpec backoffRetryFor404ResourceNotFound() {
17+
return backoffRetry(404, "Request_ResourceNotFound");
18+
}
19+
20+
// for MSGraph API, when appId of the service principal does not reference a valid application object
21+
static RetryBackoffSpec backoffRetryFor400BadRequest() {
22+
return backoffRetry(400, "Request_BadRequest");
23+
}
24+
25+
// for Microsoft.Authorization API
26+
static RetryBackoffSpec backoffRetryFor400PrincipalNotFound() {
27+
return backoffRetry(400, "PrincipalNotFound");
28+
}
29+
30+
private static RetryBackoffSpec backoffRetry(int statusCode, String errorCode) {
31+
return Retry
32+
// 10 + 20 + 40 = 70 seconds
33+
.backoff(3, ResourceManagerUtils.InternalRuntimeContext.getDelayDuration(Duration.ofSeconds(10)))
34+
.filter(throwable -> {
35+
boolean resourceNotFoundException = false;
36+
if (throwable instanceof ManagementException) {
37+
ManagementException exception = (ManagementException) throwable;
38+
if (exception.getResponse().getStatusCode() == statusCode
39+
&& exception.getValue() != null
40+
&& errorCode.equalsIgnoreCase(exception.getValue().getCode())) {
41+
resourceNotFoundException = true;
42+
}
43+
}
44+
return resourceNotFoundException;
45+
})
46+
// do not convert to RetryExhaustedException
47+
.onRetryExhaustedThrow((spec, signal) -> signal.failure());
48+
}
49+
}

sdk/resourcemanager/azure-resourcemanager-authorization/src/main/java/com/azure/resourcemanager/authorization/implementation/RoleAssignmentImpl.java

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package com.azure.resourcemanager.authorization.implementation;
55

6-
import com.azure.core.management.exception.ManagementException;
76
import com.azure.core.util.logging.ClientLogger;
87
import com.azure.resourcemanager.authorization.AuthorizationManager;
98
import com.azure.resourcemanager.authorization.models.ActiveDirectoryGroup;
@@ -17,14 +16,7 @@
1716
import com.azure.resourcemanager.resources.models.ResourceGroup;
1817
import com.azure.resourcemanager.resources.fluentcore.arm.models.Resource;
1918
import com.azure.resourcemanager.resources.fluentcore.model.implementation.CreatableImpl;
20-
import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils;
21-
import reactor.core.Exceptions;
22-
import reactor.core.publisher.Flux;
2319
import reactor.core.publisher.Mono;
24-
import reactor.util.retry.Retry;
25-
26-
import java.time.Duration;
27-
import java.util.Locale;
2820

2921
/** Implementation for ServicePrincipal and its parent interfaces. */
3022
class RoleAssignmentImpl extends CreatableImpl<RoleAssignment, RoleAssignmentInner, RoleAssignmentImpl>
@@ -93,33 +85,8 @@ public Mono<RoleAssignment> createResourceAsync() {
9385
.roleServiceClient()
9486
.getRoleAssignments()
9587
.createAsync(scope(), name(), roleAssignmentPropertiesInner)
96-
.retryWhen(Retry.withThrowable(
97-
throwableFlux ->
98-
throwableFlux
99-
.zipWith(
100-
Flux.range(1, 30),
101-
(throwable, integer) -> {
102-
if (throwable instanceof ManagementException) {
103-
ManagementException managementException =
104-
(ManagementException) throwable;
105-
String exceptionMessage =
106-
managementException.getMessage().toLowerCase(Locale.ROOT);
107-
if (exceptionMessage.contains("principalnotfound")
108-
|| exceptionMessage.contains("does not exist in the directory")) {
109-
/*
110-
* ref:
111-
* https://github.com/Azure/azure-cli/blob/dev/src/command_modules/azure-cli-role/azure/cli/command_modules/role/custom.py#L1048-L1065
112-
*/
113-
return integer;
114-
} else {
115-
throw logger.logExceptionAsError(Exceptions.propagate(throwable));
116-
}
117-
} else {
118-
throw logger.logExceptionAsError(Exceptions.propagate(throwable));
119-
}
120-
})
121-
.flatMap(i -> Mono.delay(ResourceManagerUtils.InternalRuntimeContext
122-
.getDelayDuration(Duration.ofSeconds(i)))))))
88+
// if the service principal is newly created (also apply to the case that MSI is new), wait for eventual consistency from AAD
89+
.retryWhen(RetryUtils.backoffRetryFor400PrincipalNotFound()))
12390
.map(innerToFluentMap(this));
12491
}
12592

sdk/resourcemanager/azure-resourcemanager-authorization/src/main/java/com/azure/resourcemanager/authorization/implementation/ServicePrincipalImpl.java

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package com.azure.resourcemanager.authorization.implementation;
55

66
import com.azure.resourcemanager.authorization.AuthorizationManager;
7+
import com.azure.resourcemanager.authorization.fluent.models.MicrosoftGraphPasswordCredentialInner;
78
import com.azure.resourcemanager.authorization.fluent.models.MicrosoftGraphServicePrincipalInner;
89
import com.azure.resourcemanager.authorization.fluent.models.ServicePrincipalsAddPasswordRequestBodyInner;
910
import com.azure.resourcemanager.authorization.models.ActiveDirectoryApplication;
@@ -17,6 +18,7 @@
1718
import com.azure.resourcemanager.resources.models.ResourceGroup;
1819
import reactor.core.publisher.Flux;
1920
import reactor.core.publisher.Mono;
21+
import reactor.util.retry.Retry;
2022

2123
import java.util.ArrayList;
2224
import java.util.Collections;
@@ -93,6 +95,8 @@ protected Mono<MicrosoftGraphServicePrincipalInner> getInnerAsync() {
9395

9496
@Override
9597
public Mono<ServicePrincipal> createResourceAsync() {
98+
Retry retry = isInCreateMode() ? RetryUtils.backoffRetryFor404ResourceNotFound() : null;
99+
96100
Mono<ServicePrincipal> sp;
97101
if (isInCreateMode()) {
98102
innerModel().withAccountEnabled(true);
@@ -102,6 +106,11 @@ public Mono<ServicePrincipal> createResourceAsync() {
102106
}
103107
sp = manager.serviceClient().getServicePrincipalsServicePrincipals()
104108
.createServicePrincipalAsync(innerModel()).map(innerToFluentMap(this));
109+
110+
if (applicationCreatable != null) {
111+
// retry on 400, if app is created with "withNewApplication"
112+
sp = sp.retryWhen(RetryUtils.backoffRetryFor400BadRequest());
113+
}
105114
} else {
106115
sp = manager().serviceClient().getServicePrincipalsServicePrincipals()
107116
.updateServicePrincipalAsync(id(), new MicrosoftGraphServicePrincipalInner()
@@ -112,7 +121,10 @@ public Mono<ServicePrincipal> createResourceAsync() {
112121
return sp
113122
.flatMap(
114123
servicePrincipal ->
115-
submitCredentialsAsync(servicePrincipal).mergeWith(submitRolesAsync(servicePrincipal)).last())
124+
submitCredentialsAsync(servicePrincipal, retry)
125+
// retry for Microsoft.Authorization is done in RoleAssignmentImpl
126+
.mergeWith(submitRolesAsync(servicePrincipal))
127+
.last())
116128
.map(
117129
servicePrincipal -> {
118130
for (PasswordCredentialImpl<?> passwordCredential : passwordCredentialsToCreate) {
@@ -128,7 +140,7 @@ public Mono<ServicePrincipal> createResourceAsync() {
128140
});
129141
}
130142

131-
private Mono<ServicePrincipal> submitCredentialsAsync(final ServicePrincipal sp) {
143+
private Mono<ServicePrincipal> submitCredentialsAsync(final ServicePrincipal servicePrincipal, Retry retry) {
132144
return Flux.defer(() ->
133145
// Flux.fromIterable(certificateCredentialsToCreate)
134146
// .flatMap(certificateCredential ->
@@ -142,20 +154,31 @@ private Mono<ServicePrincipal> submitCredentialsAsync(final ServicePrincipal sp)
142154
// new ServicePrincipalsRemoveKeyRequestBody()
143155
// .withKeyId(UUID.fromString(id)))),
144156
Flux.fromIterable(passwordCredentialsToCreate)
145-
.flatMap(passwordCredential ->
146-
manager().serviceClient().getServicePrincipals()
147-
.addPasswordAsync(id(),
148-
new ServicePrincipalsAddPasswordRequestBodyInner()
149-
.withPasswordCredential(passwordCredential.innerModel()))
150-
.doOnNext(passwordCredential::setInner)
151-
)
157+
.flatMap(passwordCredential -> {
158+
Mono<MicrosoftGraphPasswordCredentialInner> monoAddPassword =
159+
manager().serviceClient().getServicePrincipals()
160+
.addPasswordAsync(id(),
161+
new ServicePrincipalsAddPasswordRequestBodyInner()
162+
.withPasswordCredential(passwordCredential.innerModel()));
163+
if (retry != null) {
164+
monoAddPassword = monoAddPassword.retryWhen(retry);
165+
}
166+
monoAddPassword = monoAddPassword.doOnNext(passwordCredential::setInner);
167+
return monoAddPassword;
168+
})
152169
// Flux.fromIterable(passwordCredentialsToDelete)
153170
// .flatMap(id -> manager().serviceClient().getServicePrincipals()
154171
// .removePasswordAsync(id(),
155172
// new ServicePrincipalsRemovePasswordRequestBody()
156173
// .withKeyId(UUID.fromString(id))))
157174
)
158-
.then(refreshAsync());
175+
.then(Mono.defer(() -> {
176+
Mono<ServicePrincipal> monoRefresh = refreshAsync();
177+
if (retry != null) {
178+
monoRefresh = monoRefresh.retryWhen(retry);
179+
}
180+
return monoRefresh;
181+
}));
159182
}
160183

161184
private Mono<ServicePrincipal> submitRolesAsync(final ServicePrincipal servicePrincipal) {
@@ -166,15 +189,13 @@ private Mono<ServicePrincipal> submitRolesAsync(final ServicePrincipal servicePr
166189
create =
167190
Flux
168191
.fromIterable(rolesToCreate.entrySet())
169-
.flatMap(
170-
roleEntry ->
171-
manager()
172-
.roleAssignments()
173-
.define(this.manager().internalContext().randomUuid())
174-
.forServicePrincipal(servicePrincipal)
175-
.withBuiltInRole(roleEntry.getValue())
176-
.withScope(roleEntry.getKey())
177-
.createAsync())
192+
.flatMap(roleEntry -> manager()
193+
.roleAssignments()
194+
.define(this.manager().internalContext().randomUuid())
195+
.forServicePrincipal(servicePrincipal)
196+
.withBuiltInRole(roleEntry.getValue())
197+
.withScope(roleEntry.getKey())
198+
.createAsync())
178199
.doOnNext(
179200
indexable ->
180201
cachedRoleAssignments.put(indexable.id(), indexable))

0 commit comments

Comments
 (0)