Merge pull request #8511 from microsoft/hanli/fixes-202407

Fixes for resource connection with managed identity
This commit is contained in:
Miller Wang 2024-08-08 15:49:22 +08:00 коммит произвёл GitHub
Родитель 279b1c11a3 410f815e27
Коммит 70390f1963
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
20 изменённых файлов: 103 добавлений и 31 удалений

Просмотреть файл

@ -112,6 +112,11 @@ All notable changes to "Azure Toolkit for IntelliJ IDEA" will be documented in t
- [3.0.7](#307)
- [3.0.6](#306)
## 3.91.0
- Added support for Managed Identity Authentication in Web App Resource Connections.
- Support update the identity configuration of Web App to connect Azure resources (Azure Storage Account/Azure Key Vault/Azure Cosmos DB for NoSQL)
- Support grant permission to managed identity to connected resource (Azure Storage Account/Azure Key Vault)
## 3.90.0
### Added
- Support IntelliJ 2024.2 EAP

Просмотреть файл

@ -12,6 +12,8 @@ import com.intellij.psi.PsiMethod;
import com.microsoft.azure.toolkit.ide.appservice.AppServiceActionsContributor;
import com.microsoft.azure.toolkit.intellij.common.RunProcessHandler;
import com.microsoft.azure.toolkit.intellij.common.RunProcessHandlerMessenger;
import com.microsoft.azure.toolkit.intellij.connector.Connection;
import com.microsoft.azure.toolkit.intellij.connector.Resource;
import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule;
import com.microsoft.azure.toolkit.intellij.connector.dotazure.DotEnvBeforeRunTaskProvider;
import com.microsoft.azure.toolkit.intellij.legacy.common.AzureRunProfileState;
@ -36,6 +38,7 @@ import com.microsoft.azure.toolkit.lib.legacy.function.configurations.FunctionCo
import com.microsoft.azuretools.telemetry.TelemetryConstants;
import com.microsoft.azuretools.telemetrywrapper.Operation;
import com.microsoft.azuretools.telemetrywrapper.TelemetryManager;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -46,6 +49,8 @@ import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class FunctionDeploymentState extends AzureRunProfileState<FunctionAppBase<?, ?, ?>> {
@ -95,6 +100,12 @@ public class FunctionDeploymentState extends AzureRunProfileState<FunctionAppBas
private void applyResourceConnection() {
if (functionDeployConfiguration.isConnectionEnabled()) {
final Set<Connection<?, ?>> identityConnection = functionDeployConfiguration.getConnections().stream().filter(Connection::isManagedIdentityConnection)
.collect(Collectors.toSet());
if (CollectionUtils.isNotEmpty(identityConnection)) {
final String resources = identityConnection.stream().map(Connection::getResource).map(Resource::getName).collect(Collectors.joining(", "));
AzureMessager.getMessager().warning(String.format("Managed Identity connection is not supported for function app, your connections connected to %s may not work as expected.", resources));
}
final DotEnvBeforeRunTaskProvider.LoadDotEnvBeforeRunTask loadDotEnvBeforeRunTask = functionDeployConfiguration.getLoadDotEnvBeforeRunTask();
final Map<String, String> appSettings = functionDeployConfiguration.getConfig().appSettings();
loadDotEnvBeforeRunTask.loadEnv().stream()

Просмотреть файл

@ -32,6 +32,8 @@ import com.microsoft.azure.toolkit.intellij.common.RunProcessHandler;
import com.microsoft.azure.toolkit.intellij.common.RunProcessHandlerMessenger;
import com.microsoft.azure.toolkit.intellij.common.help.AzureWebHelpProvider;
import com.microsoft.azure.toolkit.intellij.connector.Connection;
import com.microsoft.azure.toolkit.intellij.connector.Resource;
import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule;
import com.microsoft.azure.toolkit.intellij.connector.dotazure.DotEnvBeforeRunTaskProvider;
import com.microsoft.azure.toolkit.intellij.function.components.connection.FunctionConnectionCreationDialog;
import com.microsoft.azure.toolkit.intellij.legacy.common.AzureRunProfileState;
@ -59,6 +61,7 @@ import com.microsoft.azuretools.telemetrywrapper.TelemetryManager;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.versioning.ComparableVersion;
@ -154,11 +157,22 @@ public class FunctionRunState extends AzureRunProfileState<Boolean> {
private void applyResourceConnection(Map<String, String> appSettings) {
if (functionRunConfiguration.isConnectionEnabled()) {
final Set<Connection<?, ?>> identityConnection = functionRunConfiguration.getConnections().stream()
.filter(Connection::isManagedIdentityConnection)
.collect(Collectors.toSet());
if (CollectionUtils.isNotEmpty(identityConnection)) {
final String resources = identityConnection.stream().map(Connection::getResource).map(Resource::getName).collect(Collectors.joining(", "));
AzureMessager.getMessager().warning(String.format("Managed Identity connection is not supported for function app, your connections connected to %s may not work as expected.", resources));
}
final DotEnvBeforeRunTaskProvider.LoadDotEnvBeforeRunTask loadDotEnvBeforeRunTask = functionRunConfiguration.getLoadDotEnvBeforeRunTask();
loadDotEnvBeforeRunTask.loadEnv().forEach(env -> appSettings.put(env.getKey(), env.getValue()));
}
}
private List<Connection<?, ?>> getConnections() {
return Optional.ofNullable(functionRunConfiguration).map(FunctionRunConfiguration::getConnections).orElse(Collections.emptyList());
}
@AzureOperation(name = "internal/function.validate_runtime")
private void validateFunctionRuntime() {
final ComparableVersion funcVersion = getFuncVersion();

Просмотреть файл

@ -145,7 +145,7 @@ public class WebAppRunState extends AzureRunProfileState<WebAppBase<?, ?, ?>> {
final String message = String.format(IDENTITY_PERMISSION_MESSAGE, identityUrl, identityName, identityPrincipal, resourceUrl, resource.getName());
final Action<?> openIdentityConfigurationAction = getOpenIdentityConfigurationAction(serviceResource);
final Action<?> grantPermissionAction = getGrantPermissionAction(serviceResource, identityPrincipal);
AzureMessager.getMessager().warning(message, openIdentityConfigurationAction, grantPermissionAction);
AzureMessager.getMessager().warning(message, grantPermissionAction, openIdentityConfigurationAction);
}
}
});

Просмотреть файл

@ -110,15 +110,16 @@ public class ContainerAppsEnvironmentCreationDialog extends AzureDialog<Containe
result.setWorkloadProfiles(workloadProfilesTable.getValue());
}
final LogAnalyticsWorkspaceModule workspaceModule = Azure.az(AzureLogAnalyticsWorkspace.class).logAnalyticsWorkspaces(result.getSubscription().getId());
final LogAnalyticsWorkspaceConfig workspaceConfig = cbWorkspace.getValue();
final LogAnalyticsWorkspace workspace;
if (workspaceConfig.isNewCreate()) {
workspace = workspaceModule.create(workspaceConfig.getName(), result.getResourceGroup().getResourceGroupName());
((LogAnalyticsWorkspaceDraft) workspace).setRegion(result.getRegion());
} else {
workspace = workspaceModule.get(workspaceConfig.getResourceId());
}
result.setLogAnalyticsWorkspace(workspace);
Optional.ofNullable(cbWorkspace.getValue()).ifPresent(workspaceConfig -> {
final LogAnalyticsWorkspace workspace;
if (workspaceConfig.isNewCreate()) {
workspace = workspaceModule.create(workspaceConfig.getName(), result.getResourceGroup().getResourceGroupName());
((LogAnalyticsWorkspaceDraft) workspace).setRegion(result.getRegion());
} else {
workspace = workspaceModule.get(workspaceConfig.getResourceId());
}
result.setLogAnalyticsWorkspace(workspace);
});
return result;
}
@ -196,6 +197,9 @@ public class ContainerAppsEnvironmentCreationDialog extends AzureDialog<Containe
this.lblWorkloadProfiles.setAllowAutoWrapping(true);
this.lblWorkloadProfiles.setCopyable(true);
this.lblWorkloadProfiles.setText(WORKLOAD_PROFILE_DESCRIPTION);
this.cbWorkspace.setRequired(true);
this.lblWorkloadProfiles.setLabelFor(cbWorkspace);
}
private void toggleEnvironmentType() {

Просмотреть файл

@ -105,8 +105,10 @@ public class SqlCosmosDBAccountResourceDefinition extends AzureServiceResource.D
env.put(String.format("%s_ENDPOINT", Connection.ENV_PREFIX), account.getDocumentEndpoint());
env.put(String.format("%s_DATABASE", Connection.ENV_PREFIX), database.getName());
if (data.getAuthenticationType() == AuthenticationType.USER_ASSIGNED_MANAGED_IDENTITY) {
Optional.ofNullable(data.getUserAssignedManagedIdentity()).map(Resource::getData)
.ifPresent(identity -> env.put(String.format("%s_CLIENT_ID", Connection.ENV_PREFIX), identity.getClientId()));
Optional.ofNullable(data.getUserAssignedManagedIdentity()).map(Resource::getData).ifPresent(identity -> {
env.put(String.format("%s_CLIENT_ID", Connection.ENV_PREFIX), identity.getClientId());
env.put("AZURE_CLIENT_ID", identity.getClientId());
});
}
return env;
}

Просмотреть файл

@ -57,7 +57,7 @@ public class KeyVaultResourceDefinition extends BaseKeyVaultResourceDefinition
@Override
public List<Pair<String, String>> getSpringPropertiesForManagedIdentity(String key, Connection<?, ?> connection) {
final ArrayList<Pair<String, String>> result = new ArrayList<>(getSpringProperties(key));
result.add(Pair.of("spring.cloud.azure.keyvault.secret.property-sources[0].credential.managed-identity-enabled", String.valueOf(Boolean.TRUE)));
// result.add(Pair.of("spring.cloud.azure.keyvault.secret.property-sources[0].credential.managed-identity-enabled", String.valueOf(Boolean.TRUE)));
if (connection.getAuthenticationType() == AuthenticationType.USER_ASSIGNED_MANAGED_IDENTITY) {
result.add(Pair.of("spring.cloud.azure.keyvault.secret.property-sources[0].credential.client-id", String.format("${%s_CLIENT_ID}", Connection.ENV_PREFIX)));
}
@ -70,8 +70,10 @@ public class KeyVaultResourceDefinition extends BaseKeyVaultResourceDefinition
final HashMap<String, String> env = new HashMap<>();
env.put(String.format("%s_ENDPOINT", Connection.ENV_PREFIX), vault.getVaultUri());
if (data.getAuthenticationType() == AuthenticationType.USER_ASSIGNED_MANAGED_IDENTITY) {
Optional.ofNullable(data.getUserAssignedManagedIdentity()).map(Resource::getData)
.ifPresent(identity -> env.put(String.format("%s_CLIENT_ID", Connection.ENV_PREFIX), identity.getClientId()));
Optional.ofNullable(data.getUserAssignedManagedIdentity()).map(Resource::getData).ifPresent(identity -> {
env.put(String.format("%s_CLIENT_ID", Connection.ENV_PREFIX), identity.getClientId());
env.put("AZURE_CLIENT_ID", identity.getClientId());
});
}
return env;
}

Просмотреть файл

@ -68,7 +68,7 @@ public class KeyVaultResourcePanel implements AzureFormJPanel<Resource<KeyVault>
if (info.getType() == AzureValidationInfo.Type.ERROR) {
return null;
}
return KeyVaultResourceDefinition.INSTANCE.define(cache);
return Optional.ofNullable(cache).map(KeyVaultResourceDefinition.INSTANCE::define).orElse(null);
}
@Override

Просмотреть файл

@ -1,6 +1,11 @@
<!-- Version: 3.88.0 -->
# What's new in Azure Toolkit for IntelliJ
## 3.91.0
- Added support for Managed Identity Authentication in Web App Resource Connections.
- Support update the identity configuration of Web App to connect Azure resources (Azure Storage Account/Azure Key Vault/Azure Cosmos DB for NoSQL)
- Support grant permission to managed identity to connected resource (Azure Storage Account/Azure Key Vault)
## 3.90.0
### Added
- Support IntelliJ 2024.2 EAP

Просмотреть файл

@ -5,6 +5,7 @@
package com.microsoft.azure.toolkit.intellij.common.task;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.Service;
@ -22,6 +23,7 @@ import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nonnull;
import javax.swing.*;
import java.util.Objects;
import java.util.Optional;
@Service
public class IntellijAzureTaskManager extends AzureTaskManager {
@ -118,7 +120,7 @@ public class IntellijAzureTaskManager extends AzureTaskManager {
@Override
public boolean isUIThread() {
return ApplicationManager.getApplication().isDispatchThread() || SwingUtilities.isEventDispatchThread();
return Optional.ofNullable(ApplicationManager.getApplication()).map(Application::isDispatchThread).orElse(false) || SwingUtilities.isEventDispatchThread();
}
@RequiredArgsConstructor

Просмотреть файл

@ -14,7 +14,6 @@ import com.microsoft.azure.toolkit.intellij.connector.Resource;
import com.microsoft.azure.toolkit.intellij.connector.spring.SpringManagedIdentitySupported;
import com.microsoft.azure.toolkit.lib.Azure;
import com.microsoft.azure.toolkit.lib.auth.AzureCloud;
import com.microsoft.azure.toolkit.lib.identities.Identity;
import com.microsoft.azure.toolkit.lib.storage.AzureStorageAccount;
import com.microsoft.azure.toolkit.lib.storage.AzuriteStorageAccount;
import com.microsoft.azure.toolkit.lib.storage.ConnectionStringStorageAccount;
@ -101,7 +100,10 @@ public class StorageAccountResourceDefinition extends BaseStorageAccountResource
env.put(ACCOUNT_NAME_KEY, account.getName());
}
if (connection.getAuthenticationType() == AuthenticationType.USER_ASSIGNED_MANAGED_IDENTITY) {
env.put(CLIENT_ID_KEY, Objects.requireNonNull(connection.getUserAssignedManagedIdentity()).getDataId());
Optional.ofNullable(connection.getUserAssignedManagedIdentity()).map(Resource::getData).ifPresent(identity -> {
env.put(String.format("%s_CLIENT_ID", Connection.ENV_PREFIX), identity.getClientId());
env.put("AZURE_CLIENT_ID", identity.getClientId());
});
}
return env;
}

Просмотреть файл

@ -80,6 +80,7 @@ public class StorageAccountResourcePanel implements AzureFormJPanel<Resource<ISt
txtConnectionString.setLabel("Connection string");
this.accountComboBox.addValueChangedListener(ignore -> Optional.ofNullable(getValue()).ifPresent(this::fireValueChangedEvent));
this.txtConnectionString.addValueChangedListener(ignore -> Optional.ofNullable(getValue()).ifPresent(this::fireValueChangedEvent));
}
private void onSelectEnvironment() {
@ -96,6 +97,7 @@ public class StorageAccountResourcePanel implements AzureFormJPanel<Resource<ISt
if (Objects.nonNull(txtConnectionString.getValidationInfo())) {
txtConnectionString.validateValueAsync();
}
Optional.ofNullable(getValue()).ifPresent(this::fireValueChangedEvent);
}
@Override
@ -146,7 +148,8 @@ public class StorageAccountResourcePanel implements AzureFormJPanel<Resource<ISt
if (account instanceof ConnectionStringStorageAccount && StringUtils.isNoneBlank(predefinedId, connectionString)) {
IntelliJSecureStore.getInstance().savePassword(StorageAccountResourceDefinition.class.getName(), predefinedId, null, connectionString);
}
return StorageAccountResourceDefinition.INSTANCE.define(account, predefinedId);
return Optional.ofNullable(account)
.map(acc -> StorageAccountResourceDefinition.INSTANCE.define(acc, predefinedId)).orElse(null);
}
@Override

Просмотреть файл

@ -161,7 +161,7 @@
<titleFont style="1"/>
</properties>
</component>
<component id="ed670" class="com.intellij.ui.components.JBLabel">
<component id="ed670" class="com.intellij.ui.components.JBLabel" binding="lblAuthType">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
<minimum-size width="100" height="24"/>
@ -191,7 +191,7 @@
</properties>
<border type="none"/>
<children>
<component id="7799a" class="com.intellij.ui.components.JBLabel">
<component id="7799a" class="com.intellij.ui.components.JBLabel" binding="lblIdentity">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false">
<minimum-size width="100" height="24"/>

Просмотреть файл

@ -69,6 +69,8 @@ public class ConnectorDialog extends AzureDialog<Connection<?, ?>> implements Az
private JPanel pnlUserAssignedManagedIdentity;
private AzureComboBox<AuthenticationType> cbAuthenticationType;
private UserAssignedManagedIdentityComboBox cbIdentity;
private JBLabel lblIdentity;
private JBLabel lblAuthType;
private SignInHyperLinkLabel signInHyperLinkLabel1;
private ResourceDefinition<?> resourceDefinition;
private ResourceDefinition<?> consumerDefinition;
@ -130,6 +132,9 @@ public class ConnectorDialog extends AzureDialog<Connection<?, ?>> implements Az
if (consumerDefinitions.size() == 1) {
this.fixConsumerType(consumerDefinitions.get(0));
}
this.lblIdentity.setLabelFor(cbIdentity);
this.lblAuthType.setLabelFor(cbAuthenticationType);
}
private void onSelectResource(Object o) {
@ -367,6 +372,18 @@ public class ConnectorDialog extends AzureDialog<Connection<?, ?>> implements Az
return AuthenticationType.SYSTEM_ASSIGNED_MANAGED_IDENTITY;
}
@Override
protected synchronized void setItems(List<? extends AuthenticationType> items) {
final ComboBoxModel<AuthenticationType> model = getModel();
final AuthenticationType value = (AuthenticationType) model.getSelectedItem();
this.removeAllItems();
items.forEach(this::addItem);
if (CollectionUtils.isNotEmpty(items)) {
model.setSelectedItem(items.contains(value) ? value : items.get(0));
}
this.refreshValue();
}
@Nonnull
@Override
protected List<ExtendableTextComponent.Extension> getExtensions() {

Просмотреть файл

@ -13,6 +13,7 @@ import com.microsoft.azure.toolkit.lib.common.action.AzureActionManager;
import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager;
import com.microsoft.azure.toolkit.lib.common.model.AbstractAzResource;
import com.microsoft.azure.toolkit.lib.common.model.AzResource;
import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
@ -67,6 +68,7 @@ public interface IManagedIdentitySupported<T extends AzResource> {
return AzureActionManager.getInstance().getAction(ResourceCommonActionsContributor.OPEN_URL).bind(url).withLabel("Open IAM Configuration");
}
@AzureOperation(name = "user/common.assign_role.identity", params = {"identity"})
public static boolean grantPermission(@Nonnull AzureServiceResource<?> data, @Nonnull String identity) {
final AzureServiceResource.Definition<?> d = data.getDefinition();
final AzResource r = data.getData();
@ -83,7 +85,7 @@ public interface IManagedIdentitySupported<T extends AzResource> {
resource.grantPermissionToIdentity(identity, role);
}
});
AzureMessager.getMessager().info(String.format("Roles (%s) have been assigned to identity (%s)?", rolesStr, identity));
AzureMessager.getMessager().info(String.format("Roles (%s) have been assigned to identity (%s).", rolesStr, identity));
return true;
} catch (final RuntimeException e) {
final String errorMessage = String.format(FAILED_TO_ASSIGN_MESSAGE, resource.getPortalUrl(), identity, e.getMessage());

Просмотреть файл

@ -21,7 +21,7 @@ import java.util.List;
import java.util.Map;
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
public class IdentityResource extends AzureServiceResource<Identity> {
public IdentityResource(@Nonnull Identity data, @Nonnull AzureServiceResource.Definition<Identity> definition) {
super(data, definition);

Просмотреть файл

@ -82,11 +82,11 @@ public interface ResourceDefinition<T> {
default List<AuthenticationType> getSupportedAuthenticationTypes() {
final List<AuthenticationType> result = new ArrayList<>();
result.add(AuthenticationType.CONNECTION_STRING);
if (this instanceof IManagedIdentitySupported) {
result.add(AuthenticationType.SYSTEM_ASSIGNED_MANAGED_IDENTITY);
result.add(AuthenticationType.USER_ASSIGNED_MANAGED_IDENTITY);
}
result.add(AuthenticationType.CONNECTION_STRING);
return result;
}

Просмотреть файл

@ -19,6 +19,7 @@ import com.microsoft.azure.toolkit.lib.common.model.AbstractAzResource;
import com.microsoft.azure.toolkit.lib.common.model.AbstractAzServiceSubscription;
import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation;
import com.microsoft.azure.toolkit.lib.common.operation.OperationBundle;
import com.microsoft.azure.toolkit.lib.common.operation.OperationContext;
import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager;
import com.microsoft.azure.toolkit.lib.identities.Identity;
import io.github.cdimascio.dotenv.internal.DotenvParser;
@ -96,6 +97,7 @@ public class Profile {
}
public synchronized Future<?> addConnection(@Nonnull Connection<?, ?> connection) {
OperationContext.action().setTelemetryProperty("authenticationType", Optional.ofNullable(connection.getAuthenticationType()).map(AuthenticationType::toString).orElse(StringUtils.EMPTY));
AzureFacet.addTo(this.module.getModule());
final Resource<?> resource = connection.getResource();
this.resourceManager.addResource(resource);

Просмотреть файл

@ -7,6 +7,7 @@
<extensions defaultExtensionNs="com.microsoft.tooling.msservices.intellij.azure">
<connectorResourceType implementation="com.microsoft.azure.toolkit.intellij.connector.ModuleResource$Definition"/>
<connectorResourceType implementation="com.microsoft.azure.toolkit.intellij.connector.keyvalue.KeyValueResource$Definition"/>
<connectorResourceType implementation="com.microsoft.azure.toolkit.intellij.connector.IdentityResource$Definition"/>
<actions implementation="com.microsoft.azure.toolkit.intellij.connector.ResourceConnectionActionsContributor"/>
</extensions>
<extensions defaultExtensionNs="com.intellij">

Просмотреть файл

@ -27,13 +27,13 @@
<change-notes>
<![CDATA[
<html>
<h2 id="3-90-0">3.90.0</h2>
<h3 id="added">Added</h3>
<h2 id="3-91-0">3.91.0</h2>
<ul>
<li>Support IntelliJ 2024.2 EAP</li>
<li>Support workload profiles environment type in Azure Container Apps<ul>
<li>Support create workload profiles during the creation of an Azure Container Apps environment.</li>
<li>Enable setting of workload profiles during the creation of container apps and function apps.</li>
<li>Added support for Managed Identity Authentication in Web App Resource Connections.<ul>
<li>Support update the identity configuration of Web App to connect Azure resources (Azure Storage Account/Azure Key Vault/Azure Cosmos DB for NoSQL)</li>
<li>Support grant permission to managed identity to connected resource (Azure Storage Account/Azure Key Vault)</li>
</ul>
</li>
</ul>
<p>You may get the full change log <a href="https://github.com/Microsoft/azure-tools-for-java/blob/develop/CHANGELOG.md">here</a></p>
</html>