/** * This package provide facilities to make it easy to work against AWS APIs that * are eventually consistent for applying resource state. Developers need to * sequence APIs calls in order to effectively apply state. When dependent * resources are not available, e.g. when associating KMS Key with CloudWatch * Log group, the key might not have propogated to region leading to failures to * associate with the log group. This framework provides developers the * facilities to deal with all of these situations using a simple functional * model. *
* The framework handles the following for developers: *
* Anatomy of an AWS Web Service call *
* Most AWS web service API calls follows a typical pattern shown below: *
*
* return initiator.initiate("networkmanager:CreateGlobalNetwork")
* //
* // Make the request object to create from the model
* //
* .translate(model ->
* CreateGlobalNetworkRequest.builder()
* .description(model.getDescription())
* .tags(Utils.cfnTagsToSdkTags(model.getTags()))
* .build())
* //
* // Make the call the create the global network. Delegate to framework to retry all errors and
* // report all errors as appropriate including service quote exceeded and others.
* //
* .call((r, c) -> {
* CreateGlobalNetworkResponse res = c.injectCredentialsAndInvokeV2(r, c.client()::createGlobalNetwork);
* GlobalNetwork network = res.globalNetwork();
* initiator.getResourceModel().setArn(network.globalNetworkArn());
* initiator.getResourceModel().setId(network.globalNetworkId());;
* return res;
* })
* //
* // Check to see if Global Network is available to use directly from the response or
* // stabilize as needed. Update model with Arn and Id as primary identifier on the model
* // object to communicate to CloudFormation about progress
* //
* .stabilize((_request, _response, _client, _model, _context) -> {
* GlobalNetworkState state = _response.globalNetwork().state();
* return state == GlobalNetworkState.AVAILABLE ||
* Utils.globalNetwork(_client, state.globalNetworkId()).state() == GlobalNetworkState.AVAILABLE;
* }).progress();
*
*
*
*
*
* initiator.initiate("networkmanager:DeleteGlobalNetwork")
* //
* // convert from ResourceModel to DeleteGlobalNetworkRequest
* //
* .translate(m ->
* DeleteGlobalNetworkRequest.builder()
* .globalNetworkId(m.getId())
* .build())
* //
* // Make the call to delete the network
* //
* .call((r, c) -> {
* try {
* return c.injectCredentialsAndInvokeV2(r, c.client()::deleteGlobalNetwork);
* } catch (ResourceNotFoundException e) {
* // Informs CloudFormation that the resources was deleted already
* throw new software.amazon.cloudformation.exceptions.ResourceNotFoundException(e);
* }
* })
* //
* // Wait for global network to transition to complete delete state, which is returned by a
* // ResourceNotFoundException from describe call below.
* //
* .stabilize(
* (_request, _response, _client, _model, _context) -> {
* //
* // if we successfully describe it it still exists!!!
* //
* try {
* globalNetwork(_client, _model.getId());
* } catch (ResourceNotFoundException e) {
* return true;
* }
* return false;
* }
* )
* .done(ignored -> ProgressEvent.success(null, context));
*
*
*
*
*
* return
* createLogGroup(initiator)
* .then(event -> updateRetentionInDays(initiator, event))
* .then(event -> associateKMSKey(initiator, event))
* // delegate finally to ReadHandler to return complete resource state
* .then(event -> new ReadHandler().handleRequest(proxy, request, event.getCallbackContext(), logger));
*
*
*
* Usually the final step in the sequence returns
* {@link software.amazon.cloudformation.proxy.OperationStatus#SUCCESS}. If any
* of the steps in between has an error the chain will be skipped to return the
* error with
* {@link software.amazon.cloudformation.proxy.OperationStatus#FAILED} status
* and an appropriate error message
* software.amazon.cloudformation.proxy.ProgressEvent#getMessage() E.g. if
* associateKMSKey had an error to associate KMS key for CloudWatchLogs to use,
* the chain would exit with FAILED stauts and appropriate exception message.
* Both {@link software.amazon.cloudformation.proxy.OperationStatus#SUCCESS} and
* {@link software.amazon.cloudformation.proxy.OperationStatus#FAILED} are pivot
* points in the chain that will skip the remainder of the chain.* When to re-use rebinding functionality for the model *
* Rebinding the model is used when the model is immutable by design and we need * to create a new instance of the model for each part in the chain. This is to * pure for functional programming constructs. Below is an example for * traversing list APIs to iterate over to find object of interest. For each * iteration the new model must be rebound. * *
*
* void discoverIfAlreadyExistsWithAlias() {
* ListAliasesResponse aliases = ListAliasesResponse.builder().build();
* final BiFunction<CallChain.Initiator<KmsClient, ListAliasesResponse, StdCallbackContext>,
* Integer,
* ProgressEvent<ListAliasesResponse, StdCallbackContext>> invoker =
* (initiator_, iteration) ->
* initiator_
* .initiate("kms:ListAliases-" + iteration)
* .translate(m -> ListAliasesRequest.builder().marker(m.nextMarker()).build())
* .call((r, c) -> c.injectCredentialsAndInvokeV2(r, c.client()::listAliases))
* .success();
* int iterationCount = 0;
* do {
* CallChain.Initiator<KmsClient, ListAliasesResponse, StdCallbackContext> initiator =
* this.initiator.rebindModel(aliases);
* ProgressEvent<ListAliasesResponse, StdCallbackContext> result = invoker.apply(initiator, iterationCount);
* if (!result.isSuccess()) {
* throw new RuntimeException("Error retrieving key aliases " + result.getMessage());
* }
* aliases = result.getResourceModel();
* AliasListEntry entry = aliases.aliases().stream().filter(e -> e.aliasName().equals(KEY_ALIAS)).findFirst()
* .orElse(null);
* if (entry != null) {
* kmsKeyId = entry.targetKeyId();
* aliasArn = entry.aliasArn();
* break;
* }
* if (aliases.nextMarker() == null) {
* break;
* }
* ++iterationCount;
* } while (kmsKeyId == null);
* }
*
*
*
* In the above code
*