AccountManager.java

// Generated by delombok at Wed Aug 07 06:36:36 UTC 2019
/*
 * This file is part of the pl.wrzasq.lambda.
 *
 * @license http://mit-license.org/ The MIT license
 * @copyright 2019 © by Rafał Wrzeszcz - Wrzasq.pl.
 */
package pl.wrzasq.lambda.cform.account.service;

import com.amazonaws.services.organizations.AWSOrganizations;
import com.amazonaws.services.organizations.model.Account;
import com.amazonaws.services.organizations.model.AccountNotFoundException;
import com.amazonaws.services.organizations.model.CreateAccountRequest;
import com.amazonaws.services.organizations.model.CreateAccountState;
import com.amazonaws.services.organizations.model.CreateAccountStatus;
import com.amazonaws.services.organizations.model.DescribeAccountRequest;
import com.amazonaws.services.organizations.model.DescribeCreateAccountStatusRequest;
import com.amazonaws.services.organizations.model.DescribeHandshakeRequest;
import com.amazonaws.services.organizations.model.Handshake;
import com.amazonaws.services.organizations.model.HandshakeParty;
import com.amazonaws.services.organizations.model.HandshakePartyType;
import com.amazonaws.services.organizations.model.HandshakeState;
import com.amazonaws.services.organizations.model.InviteAccountToOrganizationRequest;
import com.amazonaws.services.organizations.model.ListParentsRequest;
import com.amazonaws.services.organizations.model.MoveAccountRequest;
import com.amazonaws.services.organizations.model.Parent;
import com.amazonaws.services.organizations.model.RemoveAccountFromOrganizationRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.wrzasq.commons.aws.cloudformation.CustomResourceResponse;
import pl.wrzasq.lambda.cform.account.model.AccountRequest;

/**
 * Organizations API implementation.
 */
public class AccountManager {
    /**
     * Default sleep interval (1 minute).
     */
    private static final long DEFAULT_SLEEP_INTERVAL = 60000;
    /**
     * Logger.
     */
    private Logger logger = LoggerFactory.getLogger(AccountManager.class);
    /**
     * AWS Organizations API client.
     */
    private AWSOrganizations organizations;
    /**
     * Sleep interval for status change checks.
     */
    private long sleepInterval = AccountManager.DEFAULT_SLEEP_INTERVAL;

    /**
     * Initializes object with given Organizations client.
     *
     * @param organizations AWS Organizations client.
     */
    public AccountManager(AWSOrganizations organizations) {
        this.organizations = organizations;
    }

    /**
     * Handles account creation.
     *
     * @param input Resource creation request.
     * @param physicalResourceId Physical ID of existing resource (in this case always null).
     * @return Data about published version.
     */
    public CustomResourceResponse<Account> provision(AccountRequest input, String physicalResourceId) {
        if (physicalResourceId != null) {
            physicalResourceId = resolveExistingAccount(physicalResourceId, input);
        }
        // new account needed
        if (physicalResourceId == null) {
            physicalResourceId = this.initializeAccount(input);
        }
        // check current location in organization structure
        Parent parent = this.organizations.listParents(new ListParentsRequest().withChildId(physicalResourceId)).getParents().get(0);
        if (!parent.getId().equals(input.getOuId())) {
            this.logger.info("Moving account {} from {} to {}.", physicalResourceId, parent.getId(), input.getOuId());
            this.organizations.moveAccount(new MoveAccountRequest().withAccountId(physicalResourceId).withSourceParentId(parent.getId()).withDestinationParentId(input.getOuId()));
        }
        Account account = this.organizations.describeAccount(new DescribeAccountRequest().withAccountId(physicalResourceId)).getAccount();
        return new CustomResourceResponse<>(account, physicalResourceId);
    }

    /**
     * Handles organization deletion.
     *
     * @param input Resource delete request.
     * @param physicalResourceId Physical ID of existing resource (if present).
     * @return Empty response.
     */
    public CustomResourceResponse<Account> delete(AccountRequest input, String physicalResourceId) {
        this.organizations.removeAccountFromOrganization(new RemoveAccountFromOrganizationRequest().withAccountId(physicalResourceId));
        this.logger.info("Removed account {} from organization - keep in mind that account still exists and needs to be terminated manually!", physicalResourceId);
        return new CustomResourceResponse<>(null, physicalResourceId);
    }

    /**
     * Manages new account for organization.
     *
     * @param input Account specification.
     * @return Account ID.
     */
    private String initializeAccount(AccountRequest input) {
        return input.getAccountId() == null ? this.createAccount(input.getEmail(), input.getAccountName(), input.getAdministratorRoleName()) : this.inviteAccount(input.getAccountId());
    }

    /**
     * Creates plain, new account.
     *
     * @param email E-mail address for root access.
     * @param name Account label.
     * @param roleName Administration role name.
     * @return Account ID.
     */
    private String createAccount(String email, String name, String roleName) {
        CreateAccountStatus status = this.organizations.createAccount(new CreateAccountRequest().withEmail(email).withAccountName(name).withRoleName(roleName)).getCreateAccountStatus();
        this.logger.info("New account creation initialized for {} ({}).", email, name);
        // wait until account creation process is finished
        while (CreateAccountState.fromValue(status.getState()) == CreateAccountState.IN_PROGRESS) {
            this.logger.info("Account creation {} in progress…", name);
            this.sleep();
            status = this.organizations.describeCreateAccountStatus(new DescribeCreateAccountStatusRequest().withCreateAccountRequestId(status.getId())).getCreateAccountStatus();
        }
        this.logger.info("Account creation status: {}.", status.getState());
        if (CreateAccountState.fromValue(status.getState()) == CreateAccountState.SUCCEEDED) {
            return status.getAccountId();
        } else {
            throw new IllegalStateException(String.format("Failed to create account for %s (%s) - reason: %s.", email, name, status.getFailureReason()));
        }
    }

    /**
     * Invites existing account to organization.
     *
     * @param accountId Existing account ID.
     * @return Account ID.
     */
    private String inviteAccount(String accountId) {
        Handshake handshake = this.organizations.inviteAccountToOrganization(new InviteAccountToOrganizationRequest().withTarget(new HandshakeParty().withType(HandshakePartyType.ACCOUNT).withId(accountId))).getHandshake();
        this.logger.info("Invited account {} to the organization", accountId);
        while (HandshakeState.fromValue(handshake.getState()) == HandshakeState.REQUESTED) {
            this.logger.info("Account {} handshake in progress…", accountId);
            this.sleep();
            handshake = this.organizations.describeHandshake(new DescribeHandshakeRequest().withHandshakeId(handshake.getId())).getHandshake();
        }
        if (HandshakeState.fromValue(handshake.getState()) == HandshakeState.ACCEPTED) {
            return accountId;
        } else {
            throw new IllegalStateException(String.format("Failed to invite account %s - status: %s.", accountId, handshake.getState()));
        }
    }

    /**
     * Resolves ID of existing resource.
     *
     * @param accountId Currently provisioned ID.
     * @param input Desired account specification.
     * @return Resolved account ID.
     */
    private String resolveExistingAccount(String accountId, AccountRequest input) {
        try {
            Account account = this.organizations.describeAccount(new DescribeAccountRequest().withAccountId(accountId)).getAccount();
            if (input.getEmail().equals(account.getEmail()) && input.getAccountName().equals(account.getName())) {
                return account.getId();
            } else {
                this.logger.warn("Account {} core data changed - will re-create account!", account.getArn());
                return null;
            }
        } catch (AccountNotFoundException error) {
            this.logger.warn("Account {} not found - resolving to new account.", accountId);
            return null;
        }
    }

    /**
     * Performs a wait.
     */
    private void sleep() {
        try {
            Thread.sleep(this.sleepInterval);
        } catch (InterruptedException error) {
            this.logger.error("Wait interval interrupted.", error);
        }
    }

    /**
     * Sleep interval for status change checks.
     */
    @SuppressWarnings("all")
    @lombok.Generated
    public void setSleepInterval(final long sleepInterval) {
        this.sleepInterval = sleepInterval;
    }
}