
com.microsoft.azure.toolkit.lib.common.model.AbstractAzResource Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
package com.microsoft.azure.toolkit.lib.common.model;
import com.azure.core.credential.TokenCredential;
import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpResponse;
import com.azure.core.management.profile.AzureProfile;
import com.azure.resourcemanager.authorization.AuthorizationManager;
import com.azure.resourcemanager.authorization.models.BuiltInRole;
import com.azure.resourcemanager.authorization.models.RoleAssignment;
import com.azure.resourcemanager.authorization.models.RoleDefinition;
import com.azure.resourcemanager.resources.fluentcore.arm.ResourceId;
import com.microsoft.azure.toolkit.lib.Azure;
import com.microsoft.azure.toolkit.lib.account.IAccount;
import com.microsoft.azure.toolkit.lib.account.IAzureAccount;
import com.microsoft.azure.toolkit.lib.common.cache.Cache1;
import com.microsoft.azure.toolkit.lib.common.event.AzureEventBus;
import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager;
import com.microsoft.azure.toolkit.lib.common.utils.Debouncer;
import com.microsoft.azure.toolkit.lib.common.utils.TailingDebouncer;
import com.microsoft.azure.toolkit.lib.resource.AzureResources;
import com.microsoft.azure.toolkit.lib.resource.GenericResourceModule;
import com.microsoft.azure.toolkit.lib.resource.ResourceGroup;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpStatus;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public abstract class AbstractAzResource, P extends AzResource, R> implements AzResource {
@Nonnull
@Getter
@ToString.Include
@EqualsAndHashCode.Include
private final String name;
@Nonnull
@Getter
@ToString.Include
@EqualsAndHashCode.Include
private final String resourceGroupName;
@Nonnull
@Getter
@EqualsAndHashCode.Include
private final AbstractAzResourceModule module;
@Nonnull
private final Cache1 cache;
@Nonnull
@ToString.Include
private final AtomicReference status;
@Nonnull
private final Debouncer fireEvents = new TailingDebouncer(this::fireStatusChangedEvent, 300);
protected AbstractAzResource(@Nonnull String name, @Nonnull String resourceGroupName, @Nonnull AbstractAzResourceModule module) {
this.name = name;
this.resourceGroupName = resourceGroupName;
this.module = module;
this.cache = new Cache1<>(this::loadRemoteFromAzure)
.onValueChanged(this::onRemoteUpdated)
.onStatusChanged(s -> fireEvents.debounce());
this.status = new AtomicReference<>(Status.UNKNOWN);
}
/**
* constructor for non-top resource only.
* {@link AbstractAzResource#getResourceGroupName() module.getParent().getResourceGroupName()} is only reliable
* if current resource is not root of resource hierarchy tree.
*/
protected AbstractAzResource(@Nonnull String name, @Nonnull AbstractAzResourceModule module) {
this(name, module.getParent().getResourceGroupName(), module);
}
/**
* copy constructor
*/
protected AbstractAzResource(@Nonnull AbstractAzResource origin) {
this.name = origin.getName();
this.resourceGroupName = origin.getResourceGroupName();
this.module = origin.getModule();
this.cache = origin.cache;
this.status = origin.status;
}
public boolean exists() {
final P parent = this.getParent();
if (StringUtils.equals(this.status.get(), Status.DELETED)) {
return false;
} else if (this.isMocked() || parent.equals(AzResource.NONE) || this instanceof AbstractAzServiceSubscription || this instanceof ResourceGroup) {
return this.remoteOptional().isPresent();
} else {
final ResourceGroup rg = this.getResourceGroup();
return Objects.nonNull(rg) && rg.exists() && parent.exists() && this.remoteOptional().isPresent();
}
}
@Override
public void refresh() {
log.debug("[{}:{}]:refresh()", this.module.getName(), this.getName());
this.invalidateCache();
AzureEventBus.emit("resource.refreshed.resource", this);
}
public void invalidateCache() {
log.debug("[{}:{}]:invalidateCache->subModules.invalidateCache()", this.module.getName(), this.getName());
this.getCachedSubModules().forEach(AbstractAzResourceModule::invalidateCache);
log.debug("[{}]:invalidateCache()", this.name);
this.cache.invalidate();
}
@Nullable
protected final R loadRemoteFromAzure() {
log.debug("[{}:{}]:loadRemote()", this.module.getName(), this.getName());
try {
return this.getModule().loadResourceFromAzure(this.getName(), this.getResourceGroupName());
} catch (final Exception e) {
log.debug("[{}:{}]:loadRemote()=EXCEPTION", this.module.getName(), this.getName(), e);
if (is404(e)) {
return null;
}
throw e;
}
}
@Nullable
public final R getRemote() {
log.debug("[{}:{}]:getRemote()", this.module.getName(), this.getName());
if (isAuthRequired()) {
Azure.az(IAzureAccount.class).account();
}
if (this.isDraftForCreating()) {
log.debug("[{}:{}]:getRemote->this.isDraftForCreating()=true", this.module.getName(), this.getName());
return null;
}
return this.cache.get();
}
protected void setRemote(R remote) {
this.cache.update(() -> remote, Status.UPDATING);
}
@Nonnull
protected Optional remoteOptional() {
return Optional.ofNullable(this.getRemote());
}
protected void onRemoteUpdated(@Nullable R newRemote, R oldRemote) {
log.debug("[{}:{}]:setRemote({})", this.module.getName(), this.getName(), newRemote);
if (oldRemote == null || newRemote == null) {
log.debug("[{}:{}]:setRemote->subModules.invalidateCache()", this.module.getName(), this.getName());
this.getCachedSubModules().forEach(AbstractAzResourceModule::invalidateCache);
}
log.debug("[{}:{}]:setRemote->this.remoteRef.set({})", this.module.getName(), this.getName(), newRemote);
if (Objects.nonNull(newRemote)) {
log.debug("[{}:{}]:setRemote->setStatus(LOADING)", this.module.getName(), this.getName());
log.debug("[{}:{}]:setRemote->this.loadStatus", this.module.getName(), this.getName());
this.updateAdditionalProperties(newRemote, oldRemote);
AzureTaskManager.getInstance().runOnPooledThread(() ->
Optional.of(newRemote).map(this::loadStatus).ifPresent(this::setStatus));
} else {
log.debug("[{}:{}]:setRemote->this.setStatus(DISCONNECTED)", this.module.getName(), this.getName());
this.deleteFromCache();
this.updateAdditionalProperties(null, oldRemote);
this.getCachedSubModules().stream().flatMap(m -> m.listCachedResources().stream()).forEach(r -> r.setRemote(null));
}
}
protected void updateAdditionalProperties(@Nullable R newRemote, @Nullable R oldRemote) {
}
@Nonnull
public String getStatus() {
if (this.isDraftForCreating()) {
return Status.CREATING;
}
String cacheStatus = this.cache.getStatus();
if (StringUtils.isBlank(cacheStatus)) {
final R remote = this.cache.getIfPresent(true);
cacheStatus = Optional.ofNullable(this.cache.getStatus()).orElse(Cache1.Status.LOADING);
}
return Cache1.Status.OK.equalsIgnoreCase(cacheStatus) ?
Optional.ofNullable(this.status.get()).orElse(Cache1.Status.LOADING) : cacheStatus;
}
public void setStatus(@Nonnull String status) {
synchronized (this.status) {
log.debug("[{}:{}]:setStatus({})", this.module.getName(), this.getName(), status);
// TODO: state engine to manage status, e.g. DRAFT -> CREATING
final String oldStatus = this.status.get();
if (!Objects.equals(oldStatus, status)) {
this.status.set(status);
fireEvents.debounce();
if (StringUtils.equalsAny(status, Status.DELETING, Status.DELETED)) {
this.getCachedSubModules().stream().flatMap(m -> m.listCachedResources().stream()).forEach(r -> r.setStatus(status));
}
}
}
}
@Nonnull
protected abstract String loadStatus(@Nonnull R remote);
private void fireStatusChangedEvent() {
log.debug("[{}]:fireStatusChangedEvent()", this.getName());
AzureEventBus.emit("resource.status_changed.resource", this);
}
@Override
public void delete() {
log.debug("[{}:{}]:delete()", this.module.getName(), this.getName());
this.doModify(() -> {
if (this.exists()) {
this.deleteFromAzure();
}
return null;
}, Status.DELETING);
this.deleteFromCache();
}
private void deleteFromAzure() {
// TODO: set status should also cover its child
log.debug("[{}:{}]:delete->module.deleteResourceFromAzure({})", this.module.getName(), this.getName(), this.getId());
try {
this.getModule().deleteResourceFromAzure(this.getId());
} catch (final Exception e) {
if (is404(e)) {
log.debug("[{}]:delete()->deleteResourceFromAzure()=SC_NOT_FOUND", this.name, e);
} else {
this.getCachedSubModules().stream().flatMap(m -> m.listCachedResources().stream()).forEach(r -> r.setStatus(Status.UNKNOWN));
throw e;
}
}
}
void deleteFromCache() {
log.debug("[{}:{}]:delete->this.setStatus(DELETED)", this.module.getName(), this.getName());
this.setStatus(Status.DELETED);
log.debug("[{}:{}]:delete->module.deleteResourceFromLocal({})", this.module.getName(), this.getName(), this.getName());
this.getModule().deleteResourceFromLocal(this.getId());
final ResourceId id = ResourceId.fromString(this.getId());
final ResourceGroup resourceGroup = this.getResourceGroup();
if (Objects.isNull(id.parent()) && Objects.nonNull(resourceGroup)) { // resource group manages top resources only
final GenericResourceModule genericResourceModule = resourceGroup.genericResources();
genericResourceModule.deleteResourceFromLocal(this.getId());
}
this.getCachedSubModules().stream().flatMap(m -> m.listCachedResources().stream()).forEach(AbstractAzResource::deleteFromCache);
}
@Nonnull
public AzResource.Draft update() {
log.debug("[{}:{}]:update()", this.module.getName(), this.getName());
log.debug("[{}:{}]:update->module.update(this)", this.module.getName(), this.getName());
return this.getModule().update(this.cast(this));
}
protected void doModify(@Nonnull Runnable body, @Nullable String status) {
this.cache.update(body, status);
}
@Nullable
protected R doModify(@Nonnull Callable body, @Nullable String status) {
return this.cache.update(body, status);
}
@Nonnull
private D cast(@Nonnull Object origin) {
//noinspection unchecked
return (D) origin;
}
@Nonnull
public String getId() {
return this.getModule().toResourceId(this.getName(), this.getResourceGroupName());
}
@Nonnull
public abstract List> getSubModules();
@Nonnull
protected List> getCachedSubModules() {
return getSubModules();
}
@Nullable
public AbstractAzResourceModule, ?, ?> getSubModule(String moduleName) {
return this.getSubModules().stream().filter(m -> m.getName().equalsIgnoreCase(moduleName)).findAny().orElse(null);
}
@Nullable
public ResourceGroup getResourceGroup() {
final String rgName = this.getResourceGroupName();
final String sid = this.getSubscriptionId();
final boolean isSubscriptionSet = StringUtils.isNotBlank(sid) && !this.isMocked() &&
!StringUtils.equalsAnyIgnoreCase(sid, "", NONE.getName());
final boolean isResourceGroupSet = isSubscriptionSet && StringUtils.isNotBlank(rgName) &&
!StringUtils.equalsAnyIgnoreCase(rgName, "", NONE.getName(), RESOURCE_GROUP_PLACEHOLDER);
if (!isResourceGroupSet) {
return null;
}
return Azure.az(AzureResources.class).groups(this.getSubscriptionId()).get(rgName, rgName);
}
@Nonnull
public P getParent() {
return this.getModule().getParent();
}
public boolean isDraft() {
return this.isDraftForCreating() || this.isDraftForUpdating();
}
public boolean isDraftForCreating() {
return this instanceof Draft && Objects.isNull(((Draft, ?>) this).getOrigin())
&& Objects.isNull(this.cache.getIfPresent())
&& !StringUtils.equalsIgnoreCase(this.status.get(), Status.DELETED)
&& !StringUtils.equalsIgnoreCase(this.status.get(), Status.ERROR);
}
public boolean isDraftForUpdating() {
return this instanceof Draft && Objects.nonNull(((Draft, ?>) this).getOrigin());
}
public boolean isAuthRequired() {
return !this.isMocked();
}
public static boolean is404(Throwable t) {
return isHttpException(t, HttpStatus.SC_NOT_FOUND);
}
public static boolean is400(Throwable t) {
return isHttpException(t, HttpStatus.SC_BAD_REQUEST);
}
/**
* @param httpStatusCode {@link HttpStatus}
*/
public static boolean isHttpException(Throwable t, int httpStatusCode) {
return isHttpException(t, r -> r.getStatusCode() == httpStatusCode);
}
public static boolean isHttpException(Throwable t, Predicate predicate) {
final Throwable cause = t instanceof HttpResponseException ? t : ExceptionUtils.getRootCause(t);
return Optional.ofNullable(cause).filter(c -> cause instanceof HttpResponseException)
.map(c -> ((HttpResponseException) c))
.map(HttpResponseException::getResponse)
.filter(predicate)
.isPresent();
}
public boolean isMocked() {
final String subscriptionId = this.getSubscriptionId();
return Subscription.MOCK_SUBSCRIPTION_ID.equals(subscriptionId) || !Character.isLetterOrDigit(subscriptionId.trim().charAt(0));
}
public void grantPermissionToIdentity(final String identity, final String role) {
final AuthorizationManager authorizationManager = getAuthorizationManager();
final String roleAssignmentName = UUID.randomUUID().toString();
authorizationManager.roleAssignments().define(roleAssignmentName)
.forObjectId(identity)
.withRoleDefinition(role)
.withScope(this.getId())
.create();
}
public void grantPermissionToIdentity(final String identity, final BuiltInRole role) {
final AuthorizationManager authorizationManager = getAuthorizationManager();
final String roleAssignmentName = UUID.randomUUID().toString();
authorizationManager.roleAssignments().define(roleAssignmentName)
.forObjectId(identity)
.withBuiltInRole(role)
.withScope(this.getId())
.create();
}
public List getRoleAssignments(final String identity) {
final AuthorizationManager authorizationManager = getAuthorizationManager();
return authorizationManager.roleAssignments()
.listByScope(this.getId()).stream()
.filter(assignment -> StringUtils.equalsIgnoreCase(assignment.principalId(), identity))
.collect(Collectors.toList());
}
public List getRoleDefinitions(final String identity) {
final AuthorizationManager authorizationManager = getAuthorizationManager();
final List roleAssignments = getRoleAssignments(identity);
return roleAssignments.stream()
.map(role -> authorizationManager.roleDefinitions().getById(role.roleDefinitionId()))
.collect(Collectors.toList());
}
public List getPermissions(final String identity) {
return getRoleDefinitions(identity).stream()
.flatMap(rd -> rd.permissions().stream())
.flatMap(p -> Stream.of(p.actions(), p.notActions(), p.dataActions(), p.notDataActions()).flatMap(List::stream)).collect(Collectors.toList());
}
// todo: add cache for different subscriptions
// todo: resource could overwrite this implementation so that they could re-use the same authorization manager from their service client
@Nonnull
protected AuthorizationManager getAuthorizationManager() {
final String subscriptionId = this.getSubscriptionId();
final IAccount account = Azure.az(IAzureAccount.class).account();
final Subscription subscription = account.getSubscription(subscriptionId);
final TokenCredential tokenCredential = account.getTokenCredential(subscriptionId);
final AzureProfile profile = new AzureProfile(subscription.getTenantId(), subscriptionId, account.getEnvironment());
return AuthorizationManager.authenticate(tokenCredential, profile);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy