All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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