diff --git a/aws_organizations/README.md b/aws_organizations/README.md index c9b1071..788d406 100644 --- a/aws_organizations/README.md +++ b/aws_organizations/README.md @@ -35,6 +35,10 @@ Before getting started, ensure you have the following prerequisites: 10. Move to the Review page and Click Submit. This launches the creation process for the Datadog StackSet. This could take a while depending on how many accounts need to be integrated. Ensure that the StackSet successfully creates all resources before proceeding. 11. After the stack is created, go back to the AWS integration tile in Datadog and click Ready! +## Pre-existing Integrations + +If you deploy this StackSet to an AWS Organization or OU that includes accounts which already have a Datadog integration configured (e.g., via a standalone stack), the template will detect the existing integration and update it rather than attempting to create a duplicate. This prevents a 409 conflict error that would otherwise trigger a rollback and delete the pre-existing integration. Additionally, if the stack fails for any other reason (e.g., IAM role name conflict) and rolls back, the rollback will preserve the pre-existing integration rather than deleting it. + ## Datadog::Integrations::AWS This CloudFormation StackSet only manages *AWS* resources required by the Datadog AWS integration. The actual integration configuration within Datadog platform can also be managed in CloudFormation using the custom resource [Datadog::Integrations::AWS](https://github.com/DataDog/datadog-cloudformation-resources/tree/master/datadog-integrations-aws-handler) if you like. diff --git a/aws_organizations/main_organizations.yaml b/aws_organizations/main_organizations.yaml index 4bb3559..5c38ddf 100644 --- a/aws_organizations/main_organizations.yaml +++ b/aws_organizations/main_organizations.yaml @@ -272,13 +272,42 @@ Resources: try: # Call Datadog API and report response back to CloudFormation uuid = "" - if event["RequestType"] != "Create": + preexisting = False + if event["RequestType"] == "Create": + # Check if account already exists to avoid 409 conflict. + # A failed POST triggers rollback which deletes the pre-existing integration, + # so we use PATCH instead of POST if the account is already registered. + datadog_account_response = get_datadog_account(event) + code = datadog_account_response.getcode() + data = datadog_account_response.read() + if code == 200 and data: + json_response = json.loads(data) + if len(json_response["data"]) > 0: + LOGGER.info("Account already exists in Datadog. Using PATCH to update instead of POST to avoid conflict.") + uuid = json_response["data"][0]["id"] + method = "PATCH" + preexisting = True + elif event["RequestType"] == "Delete": + # If this resource was created by patching a pre-existing integration, + # do not delete it on stack rollback/deletion. + physical_resource_id = event.get("PhysicalResourceId", "") + if physical_resource_id == "PREEXISTING": + LOGGER.info("Integration was pre-existing. Skipping delete to preserve existing integration.") + cfResponse = {"Message": "Skipped delete for pre-existing integration."} + cfnresponse.send(event, context, responseStatus="SUCCESS", responseData=cfResponse, reason=None) + return + datadog_account_response = get_datadog_account(event) + uuid = extract_uuid_from_account_response(event, context, datadog_account_response) + if uuid is None: + return + else: datadog_account_response = get_datadog_account(event) uuid = extract_uuid_from_account_response(event, context, datadog_account_response) if uuid is None: return response = call_datadog_api(uuid, event, method) - cfn_response_send_api_result(event, context, method, response) + physical_id = "PREEXISTING" if preexisting else None + cfn_response_send_api_result(event, context, method, response, physical_id) except Exception as e: LOGGER.info("Failed - exception thrown during processing.") @@ -315,7 +344,7 @@ Resources: return None - def cfn_response_send_api_result(event, context, method, response): + def cfn_response_send_api_result(event, context, method, response, physical_id=None): reason = None json_response = "" code = response.getcode() @@ -331,13 +360,14 @@ Resources: if method == "POST" or method == "PATCH": external_id = json_response["data"]["attributes"]["auth_config"]["external_id"] cfResponse["ExternalId"] = external_id - cfnresponse.send( - event, - context, - responseStatus=response_status, - responseData=cfResponse, - reason=reason, - ) + send_kwargs = { + "responseStatus": response_status, + "responseData": cfResponse, + "reason": reason, + } + if physical_id: + send_kwargs["physicalResourceId"] = physical_id + cfnresponse.send(event, context, **send_kwargs) return cfn_response_send_failure(event, context, "Datadog API returned error: {}".format(json_response)) diff --git a/aws_organizations/version.txt b/aws_organizations/version.txt index b913b7c..52cad5e 100644 --- a/aws_organizations/version.txt +++ b/aws_organizations/version.txt @@ -1 +1 @@ -v4.1.0 +v4.1.1 diff --git a/aws_quickstart/README.md b/aws_quickstart/README.md index 43f39ad..753f167 100644 --- a/aws_quickstart/README.md +++ b/aws_quickstart/README.md @@ -23,6 +23,10 @@ This template creates the following AWS resources required by the Datadog AWS in - The Datadog Forwarder only deploys to the AWS region where the AWS integration CloudFormation stack is launched. If you operate in multiple AWS regions, you can deploy the Forwarder stack (without the rest of the AWS integration stack) directly to other regions as needed. - The Datadog Forwarder is installed with default settings as a nested stack, edit the nested stack directly to update the forwarder specific settings. +## Pre-existing Integrations + +If you deploy this CloudFormation stack to an AWS account that already has a Datadog integration configured, the stack will detect the existing integration and update it rather than attempting to create a duplicate. This prevents a 409 conflict error that would otherwise trigger a rollback and delete the pre-existing integration. Additionally, if the stack fails for any other reason (e.g., IAM role name conflict) and rolls back, the rollback will preserve the pre-existing integration rather than deleting it. + ## Updating your CloudFormation Stack As of v2.0.0 of the aws_quickstart template updates to the stack parameters are supported. Updates should generally be made to the root CloudFormation Stack (entitled DatadogIntegration by default). We do not support updating the API Key or APP Key fields to point to a different Datadog Organization - if these are updated they must point to the original Organization. You can also update the version of the template used by selecting "Replace existing template" while updating your CloudFormation Stack. You must select a version number with the same major version as your current template. diff --git a/aws_quickstart/datadog_integration_api_call_v2.yaml b/aws_quickstart/datadog_integration_api_call_v2.yaml index 18d0618..435c95b 100644 --- a/aws_quickstart/datadog_integration_api_call_v2.yaml +++ b/aws_quickstart/datadog_integration_api_call_v2.yaml @@ -237,13 +237,42 @@ Resources: try: # Call Datadog API and report response back to CloudFormation uuid = "" - if event["RequestType"] != "Create": + preexisting = False + if event["RequestType"] == "Create": + # Check if account already exists to avoid 409 conflict. + # A failed POST triggers rollback which deletes the pre-existing integration, + # so we use PATCH instead of POST if the account is already registered. + datadog_account_response = get_datadog_account(event) + code = datadog_account_response.getcode() + data = datadog_account_response.read() + if code == 200 and data: + json_response = json.loads(data) + if len(json_response["data"]) > 0: + LOGGER.info("Account already exists in Datadog. Using PATCH to update instead of POST to avoid conflict.") + uuid = json_response["data"][0]["id"] + method = "PATCH" + preexisting = True + elif event["RequestType"] == "Delete": + # If this resource was created by patching a pre-existing integration, + # do not delete it on stack rollback/deletion. + physical_resource_id = event.get("PhysicalResourceId", "") + if physical_resource_id == "PREEXISTING": + LOGGER.info("Integration was pre-existing. Skipping delete to preserve existing integration.") + cfResponse = {"Message": "Skipped delete for pre-existing integration."} + cfnresponse.send(event, context, responseStatus="SUCCESS", responseData=cfResponse, reason=None) + return + datadog_account_response = get_datadog_account(event) + uuid = extract_uuid_from_account_response(event, context, datadog_account_response) + if uuid is None: + return + else: datadog_account_response = get_datadog_account(event) uuid = extract_uuid_from_account_response(event, context, datadog_account_response) if uuid is None: return response = call_datadog_api(uuid, event, method) - cfn_response_send_api_result(event, context, method, response) + physical_id = "PREEXISTING" if preexisting else None + cfn_response_send_api_result(event, context, method, response, physical_id) except Exception as e: LOGGER.info("Failed - exception thrown during processing.") @@ -280,7 +309,7 @@ Resources: return None - def cfn_response_send_api_result(event, context, method, response): + def cfn_response_send_api_result(event, context, method, response, physical_id=None): reason = None json_response = "" code = response.getcode() @@ -296,13 +325,14 @@ Resources: if method == "POST" or method == "PATCH": external_id = json_response["data"]["attributes"]["auth_config"]["external_id"] cfResponse["ExternalId"] = external_id - cfnresponse.send( - event, - context, - responseStatus=response_status, - responseData=cfResponse, - reason=reason, - ) + send_kwargs = { + "responseStatus": response_status, + "responseData": cfResponse, + "reason": reason, + } + if physical_id: + send_kwargs["physicalResourceId"] = physical_id + cfnresponse.send(event, context, **send_kwargs) return cfn_response_send_failure(event, context, "Datadog API returned error: {}".format(json_response)) diff --git a/aws_quickstart/version.txt b/aws_quickstart/version.txt index 5ce26e9..237fc3e 100644 --- a/aws_quickstart/version.txt +++ b/aws_quickstart/version.txt @@ -1 +1 @@ -v4.5.2 +v4.5.3