Skip to content

Commit

Permalink
feat(search events): implement search event emitter and consumer (#666)
Browse files Browse the repository at this point in the history
* feat(search events): initial scaffolding

* feat(search/snowplow): search event emitter and consumer

Emit search result events from user-list-search
for corpus searches, to the event bus.

Add consumer to shared snowplow event consumer.

[POCKET-10238]

* chore: fix snowplow tests and update snowtype

* chore: fixes for lint and typescript

Removed the unnecessary snowplow codgen -- will
need to be repeated if they are regenerated unless
the version becomes compatible with the new typescript?
  • Loading branch information
kschelonka authored Jul 22, 2024
1 parent dbaa24b commit f16a902
Show file tree
Hide file tree
Showing 37 changed files with 4,449 additions and 1,956 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const eventConfig = {
PocketSearch: {
name: 'SearchApiEvents',
source: 'search-api-events',
detailType: ['corpus_search_result'],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Construct } from 'constructs';
import {
PocketEventBridgeProps,
PocketEventBridgeRuleWithMultipleTargets,
ApplicationEventBus,
} from '@pocket-tools/terraform-modules';
import { config } from '../../config';
import {
sqsQueue,
snsTopic,
dataAwsIamPolicyDocument,
snsTopicPolicy,
dataAwsSnsTopic,
} from '@cdktf/provider-aws';
import { resource } from '@cdktf/provider-null';
import { eventConfig } from './eventConfig';
import { createDeadLetterQueueAlarm } from '../utils';

export class SearchApiEvents extends Construct {
public readonly snsTopic: snsTopic.SnsTopic;
public readonly snsTopicDlq: sqsQueue.SqsQueue;

constructor(
scope: Construct,
private name: string,
private sharedEventBus: ApplicationEventBus,
private snsAlarmTopic: dataAwsSnsTopic.DataAwsSnsTopic,
) {
super(scope, name);

this.snsTopic = new snsTopic.SnsTopic(this, 'search-api-event-topic', {
name: `${config.prefix}-SearchApiEventTopic`,
lifecycle: {
preventDestroy: true,
},
});

this.snsTopicDlq = new sqsQueue.SqsQueue(this, 'sns-topic-dql', {
name: `${config.prefix}-SNS-${eventConfig.PocketSearch.name}-Topic-DLQ`,
tags: config.tags,
});

const searchEvent = this.createSearchApiEventRules();
this.createPolicyForEventBridgeToSns();

//get alerted if we get 10 messages in DLQ in 4 evaluation period of 5 minutes (for shareable-list)
createDeadLetterQueueAlarm(
this,
snsAlarmTopic,
this.snsTopicDlq.name,
`${eventConfig.PocketSearch.name}-Rule-dlq-alarm`,
true,
4,
300,
10,
);

//place-holder resource used to make sure we are not
//removing the event-rule or the SNS by mistake
//if the resources are removed, this would act as an additional check
//to prevent resource deletion in-addition to preventDestroy
//e.g removing any of the dependsOn resource and running npm build would
//throw error
new resource.Resource(this, 'null-resource', {
dependsOn: [searchEvent.getEventBridge().rule, this.snsTopic],
});
}

/**
* Rolls out event bridge rule and attaches them to sns target
* for searchapi-events
* @private
*/
private createSearchApiEventRules() {
const searchEventRuleProps: PocketEventBridgeProps = {
eventRule: {
name: `${config.prefix}-${eventConfig.PocketSearch.name}-Rule`,
eventPattern: {
source: [eventConfig.PocketSearch.source],
'detail-type': eventConfig.PocketSearch.detailType,
},
eventBusName: this.sharedEventBus.bus.name,
preventDestroy: true,
},
targets: [
{
arn: this.snsTopic.arn,
deadLetterArn: this.snsTopicDlq.arn,
targetId: `${config.prefix}-SearchApi-Event-SNS-Target`,
terraformResource: this.snsTopic,
},
],
};
return new PocketEventBridgeRuleWithMultipleTargets(
this,
`${config.prefix}-SearchApi-EventBridge-Rule`,
searchEventRuleProps,
);
}

private createPolicyForEventBridgeToSns() {
const eventBridgeSnsPolicy =
new dataAwsIamPolicyDocument.DataAwsIamPolicyDocument(
this,
`${config.prefix}-EventBridge-SNS-Policy`,
{
statement: [
{
effect: 'Allow',
actions: ['sns:Publish'],
resources: [this.snsTopic.arn],
principals: [
{
identifiers: ['events.amazonaws.com'],
type: 'Service',
},
],
},
],
},
).json;

return new snsTopicPolicy.SnsTopicPolicy(
this,
'searchapi-events-sns-topic-policy',
{
arn: this.snsTopic.arn,
policy: eventBridgeSnsPolicy,
},
);
}
}
8 changes: 8 additions & 0 deletions infrastructure/pocket-event-bridge/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { UserRegistrationEventSchema } from './events-schema/userRegistrationEve
import { AllEventsRule } from './event-rules/all-events/allEventRules';
import { ForgotPassword as ForgotPasswordRequest } from './event-rules/forgot-password-request';
import { SharesApiEvents } from './event-rules/shares-api-events/pocketShareEventRules';
import { SearchApiEvents } from './event-rules/search-api-events/pocketSearchEventRules';
import { CorpusEvents } from './event-rules/corpus-events/corpusEventRules';

class PocketEventBus extends TerraformStack {
Expand Down Expand Up @@ -138,6 +139,13 @@ class PocketEventBus extends TerraformStack {
sharedPocketEventBus,
alarmSnsTopic,
);
// search-api events
new SearchApiEvents(
this,
'search-api-events',
sharedPocketEventBus,
alarmSnsTopic,
);
// Corpus Events (uses the default bus, not the shared event bus)
new CorpusEvents(this, 'corpus-events', alarmSnsTopic);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const config = {
shareableListItemEventTopic: 'ShareableListItemEventTopic',
collectionEventTopic: 'CollectionEventTopic',
sharesApiEventTopic: 'SharesApiEventTopic',
searchApiEventTopic: 'SearchApiEventTopic',
},
envVars: {
snowplowEndpoint: snowplowEndpoint,
Expand Down
10 changes: 10 additions & 0 deletions infrastructure/shared-snowplow-consumer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ class SnowplowSharedConsumerStack extends TerraformStack {
config.eventBridge.sharesApiEventTopic,
);

// Consumer Queue should be able to listen to search result events from search-api
const searchApiEventTopicArn = `arn:aws:sns:${region.name}:${caller.accountId}:${config.eventBridge.prefix}-${config.environment}-${config.eventBridge.searchApiEventTopic}`;
this.subscribeSqsToSnsTopic(
sqsConsumeQueue,
snsTopicDlq,
searchApiEventTopicArn,
config.eventBridge.searchApiEventTopic,
);

// Consumer Queue should be able to listen to collection-created and collection-updated events from collection-api.
const collectionEventTopicArn = `arn:aws:sns:${region.name}:${caller.accountId}:${config.eventBridge.prefix}-${config.environment}-${config.eventBridge.collectionEventTopic}`;
this.subscribeSqsToSnsTopic(
Expand All @@ -136,6 +145,7 @@ class SnowplowSharedConsumerStack extends TerraformStack {
shareableListItemEventTopicArn,
collectionEventTopicArn,
sharesApiEventTopicArn,
searchApiEventTopicArn,
];

// Assigns inline access policy for SQS and DLQ.
Expand Down
4 changes: 4 additions & 0 deletions infrastructure/user-list-search/apollo_ecs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ module "apollo" {
name = "ELASTICSEARCH_INDEX"
value = local.elastic_index
},
{
name = "EVENT_BUS_NAME"
value = local.event_bus_name
},
{
name = "CORPUS_INDEX_EN"
value = local.corpus_index_en
Expand Down
1 change: 1 addition & 0 deletions infrastructure/user-list-search/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ locals {
container_name = "node"
container_credential = "arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:Shared/DockerHub"
elastic_index = "list"
event_bus_name = "PocketEventBridge-${local.env}-Shared-Event-Bus"
corpus_index_en = "corpus_en"
corpus_index_fr = "corpus_fr"
corpus_index_es = "corpus_es"
Expand Down
Loading

3 comments on commit f16a902

@pocket-github-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan Result (user-list-search-cdk)

CI link

⚠️ Resource Deletion will happen ⚠️

This plan contains resource delete operation. Please check the plan result very carefully!

Plan: 2 to add, 0 to change, 2 to destroy.
  • Replace
    • aws_ecs_task_definition.apollo
    • null_resource.apollo_update-task-definition
Change Result (Click me)
  # aws_ecs_task_definition.apollo must be replaced
+/- resource "aws_ecs_task_definition" "apollo" {
      ~ arn                      = "arn:aws:ecs:us-east-1:996905175585:task-definition/UserListSearch-Prod-Apollo:552" -> (known after apply)
      ~ arn_without_revision     = "arn:aws:ecs:us-east-1:996905175585:task-definition/UserListSearch-Prod-Apollo" -> (known after apply)
      ~ container_definitions    = jsonencode(
          ~ [
              ~ {
                  - cpu                    = 0
                  - environment            = []
                  - mountPoints            = []
                    name                   = "aws-otel-collector"
                  ~ portMappings           = [
                      ~ {
                          - protocol      = "tcp"
                            # (2 unchanged attributes hidden)
                        },
                      ~ {
                          - protocol      = "tcp"
                            # (2 unchanged attributes hidden)
                        },
                    ]
                  - systemControls         = []
                  - volumesFrom            = []
                    # (6 unchanged attributes hidden)
                },
              ~ {
                  - cpu                    = 0
                  ~ environment            = [
                        # (9 unchanged elements hidden)
                        {
                            name  = "ELASTICSEARCH_INDEX"
                            value = "list"
                        },
                      + {
                          + name  = "EVENT_BUS_NAME"
                          + value = "PocketEventBridge-Prod-Shared-Event-Bus"
                        },
                        {
                            name  = "NODE_ENV"
                            value = "production"
                        },
                        # (3 unchanged elements hidden)
                    ]
                  - mountPoints            = []
                    name                   = "node"
                  - systemControls         = []
                  - volumesFrom            = []
                    # (6 unchanged attributes hidden)
                },
            ] # forces replacement
        )
      ~ id                       = "UserListSearch-Prod-Apollo" -> (known after apply)
      ~ revision                 = 552 -> (known after apply)
        tags                     = {
            "app_code"       = "pocket"
            "component_code" = "pocket-userlistsearch"
            "costCenter"     = "Pocket"
            "env_code"       = "prod"
            "environment"    = "Prod"
            "owner"          = "Pocket"
            "service"        = "UserListSearch"
        }
        # (12 unchanged attributes hidden)
    }

  # null_resource.apollo_update-task-definition must be replaced
-/+ resource "null_resource" "apollo_update-task-definition" {
      ~ id       = "5904693141729543027" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "task_arn" = "arn:aws:ecs:us-east-1:996905175585:task-definition/UserListSearch-Prod-Apollo:552" -> (known after apply)
        }
    }

Plan: 2 to add, 0 to change, 2 to destroy.

Changes to Outputs:
  ~ ecs-task-arn           = "arn:aws:ecs:us-east-1:996905175585:task-definition/UserListSearch-Prod-Apollo:552" -> (known after apply)

@pocket-github-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan Result (shared-snowplow-consumer-cdk)

CI link

Plan: 1 to add, 2 to change, 0 to destroy.
  • Create
    • aws_sns_topic_subscription.SearchApiEventTopic-sns-subscription
  • Update
    • aws_sqs_queue_policy.shared-snowplow-consumer-sns-dlq-policy
    • aws_sqs_queue_policy.shared-snowplow-consumer-sns-sqs-policy
Change Result (Click me)
  # aws_sns_topic_subscription.SearchApiEventTopic-sns-subscription will be created
  + resource "aws_sns_topic_subscription" "SearchApiEventTopic-sns-subscription" {
      + arn                             = (known after apply)
      + confirmation_timeout_in_minutes = 1
      + confirmation_was_authenticated  = (known after apply)
      + endpoint                        = "arn:aws:sqs:us-east-1:996905175585:SharedSnowplowConsumer-Prod-SharedEventConsumer-Queue"
      + endpoint_auto_confirms          = false
      + filter_policy_scope             = (known after apply)
      + id                              = (known after apply)
      + owner_id                        = (known after apply)
      + pending_confirmation            = (known after apply)
      + protocol                        = "sqs"
      + raw_message_delivery            = false
      + redrive_policy                  = jsonencode(
            {
              + deadLetterTargetArn = "arn:aws:sqs:us-east-1:996905175585:SharedSnowplowConsumer-Prod-SNS-Topics-DLQ"
            }
        )
      + topic_arn                       = "arn:aws:sns:us-east-1:996905175585:PocketEventBridge-Prod-SearchApiEventTopic"
    }

  # aws_sqs_queue_policy.shared-snowplow-consumer-sns-dlq-policy will be updated in-place
  ~ resource "aws_sqs_queue_policy" "shared-snowplow-consumer-sns-dlq-policy" {
        id        = "https://sqs.us-east-1.amazonaws.com/996905175585/SharedSnowplowConsumer-Prod-SNS-Topics-DLQ"
      ~ policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ ArnLike = {
                              ~ "aws:SourceArn" = [
                                    # (5 unchanged elements hidden)
                                    "arn:aws:sns:us-east-1:996905175585:PocketEventBridge-Prod-SharesApiEventTopic",
                                  + "arn:aws:sns:us-east-1:996905175585:PocketEventBridge-Prod-SearchApiEventTopic",
                                ]
                            }
                        }
                        # (4 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # aws_sqs_queue_policy.shared-snowplow-consumer-sns-sqs-policy will be updated in-place
  ~ resource "aws_sqs_queue_policy" "shared-snowplow-consumer-sns-sqs-policy" {
        id        = "https://sqs.us-east-1.amazonaws.com/996905175585/SharedSnowplowConsumer-Prod-SharedEventConsumer-Queue"
      ~ policy    = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Condition = {
                          ~ ArnLike = {
                              ~ "aws:SourceArn" = [
                                    # (5 unchanged elements hidden)
                                    "arn:aws:sns:us-east-1:996905175585:PocketEventBridge-Prod-SharesApiEventTopic",
                                  + "arn:aws:sns:us-east-1:996905175585:PocketEventBridge-Prod-SearchApiEventTopic",
                                ]
                            }
                        }
                        # (4 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

Plan: 1 to add, 2 to change, 0 to destroy.

@pocket-github-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan Result (pocket-event-bridge-cdk)

CI link

Plan: 7 to add, 0 to change, 0 to destroy.
  • Create
    • aws_cloudwatch_event_rule.search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_BED5FF67
    • aws_cloudwatch_event_target.search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_event-bridge-target-PocketEventBridge-Prod-SearchApi-Event-SNS-Target_D4C84C6A
    • aws_cloudwatch_metric_alarm.search-api-events_searchapievents-rule-dlq-alarm_6C7B6DB6
    • aws_sns_topic.search-api-events_search-api-event-topic_9202A150
    • aws_sns_topic_policy.search-api-events_searchapi-events-sns-topic-policy_9270FFC6
    • aws_sqs_queue.search-api-events_sns-topic-dql_653D177A
    • null_resource.search-api-events_null-resource_43620CE7
Change Result (Click me)
  # data.aws_iam_policy_document.search-api-events_PocketEventBridge-Prod-EventBridge-SNS-Policy_AF4B1F50 will be read during apply
  # (config refers to values not yet known)
 <= data "aws_iam_policy_document" "search-api-events_PocketEventBridge-Prod-EventBridge-SNS-Policy_AF4B1F50" {
      + id            = (known after apply)
      + json          = (known after apply)
      + minified_json = (known after apply)

      + statement {
          + actions   = [
              + "sns:Publish",
            ]
          + effect    = "Allow"
          + resources = [
              + (known after apply),
            ]

          + principals {
              + identifiers = [
                  + "events.amazonaws.com",
                ]
              + type        = "Service"
            }
        }
    }

  # aws_cloudwatch_event_rule.search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_BED5FF67 will be created
  + resource "aws_cloudwatch_event_rule" "search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_BED5FF67" {
      + arn            = (known after apply)
      + event_bus_name = "PocketEventBridge-Prod-Shared-Event-Bus"
      + event_pattern  = jsonencode(
            {
              + detail-type = [
                  + "corpus_search_result",
                ]
              + source      = [
                  + "search-api-events",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "PocketEventBridge-Prod-SearchApiEvents-Rule-Rule"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "app_code"       = "pocket-content-shared"
          + "component_code" = "pocket-content-shared-pocketeventbridge"
          + "costCenter"     = "Shared"
          + "env_code"       = "prod"
          + "environment"    = "Prod"
          + "owner"          = "Pocket"
          + "service"        = "PocketEventBridge"
        }
    }

  # aws_cloudwatch_event_target.search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_event-bridge-target-PocketEventBridge-Prod-SearchApi-Event-SNS-Target_D4C84C6A will be created
  + resource "aws_cloudwatch_event_target" "search-api-events_PocketEventBridge-Prod-SearchApi-EventBridge-Rule_event-bridge-rule_event-bridge-target-PocketEventBridge-Prod-SearchApi-Event-SNS-Target_D4C84C6A" {
      + arn            = (known after apply)
      + event_bus_name = "PocketEventBridge-Prod-Shared-Event-Bus"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "PocketEventBridge-Prod-SearchApiEvents-Rule-Rule"
      + target_id      = "PocketEventBridge-Prod-SearchApi-Event-SNS-Target"

      + dead_letter_config {
          + arn = (known after apply)
        }
    }

  # aws_cloudwatch_metric_alarm.search-api-events_searchapievents-rule-dlq-alarm_6C7B6DB6 will be created
  + resource "aws_cloudwatch_metric_alarm" "search-api-events_searchapievents-rule-dlq-alarm_6C7B6DB6" {
      + actions_enabled                       = true
      + alarm_actions                         = [
          + "arn:aws:sns:us-east-1:996905175585:Backend-Prod-ChatBot",
        ]
      + alarm_description                     = "Number of messages >= 10"
      + alarm_name                            = "PocketEventBridge-Prod-SearchApiEvents-Rule-dlq-alarm"
      + arn                                   = (known after apply)
      + comparison_operator                   = "GreaterThanOrEqualToThreshold"
      + dimensions                            = {
          + "QueueName" = "PocketEventBridge-Prod-SNS-SearchApiEvents-Topic-DLQ"
        }
      + evaluate_low_sample_count_percentiles = (known after apply)
      + evaluation_periods                    = 4
      + id                                    = (known after apply)
      + metric_name                           = "ApproximateNumberOfMessagesVisible"
      + namespace                             = "AWS/SQS"
      + ok_actions                            = [
          + "arn:aws:sns:us-east-1:996905175585:Backend-Prod-ChatBot",
        ]
      + period                                = 300
      + statistic                             = "Sum"
      + tags_all                              = {
          + "app_code"       = "pocket-content-shared"
          + "component_code" = "pocket-content-shared-pocketeventbridge"
          + "costCenter"     = "Shared"
          + "env_code"       = "prod"
          + "environment"    = "Prod"
          + "owner"          = "Pocket"
          + "service"        = "PocketEventBridge"
        }
      + threshold                             = 10
      + treat_missing_data                    = "missing"
    }

  # aws_sns_topic.search-api-events_search-api-event-topic_9202A150 will be created
  + resource "aws_sns_topic" "search-api-events_search-api-event-topic_9202A150" {
      + arn                         = (known after apply)
      + beginning_archive_time      = (known after apply)
      + content_based_deduplication = false
      + fifo_topic                  = false
      + id                          = (known after apply)
      + name                        = "PocketEventBridge-Prod-SearchApiEventTopic"
      + name_prefix                 = (known after apply)
      + owner                       = (known after apply)
      + policy                      = (known after apply)
      + signature_version           = (known after apply)
      + tags_all                    = {
          + "app_code"       = "pocket-content-shared"
          + "component_code" = "pocket-content-shared-pocketeventbridge"
          + "costCenter"     = "Shared"
          + "env_code"       = "prod"
          + "environment"    = "Prod"
          + "owner"          = "Pocket"
          + "service"        = "PocketEventBridge"
        }
      + tracing_config              = (known after apply)
    }

  # aws_sns_topic_policy.search-api-events_searchapi-events-sns-topic-policy_9270FFC6 will be created
  + resource "aws_sns_topic_policy" "search-api-events_searchapi-events-sns-topic-policy_9270FFC6" {
      + arn    = (known after apply)
      + id     = (known after apply)
      + owner  = (known after apply)
      + policy = (known after apply)
    }

  # aws_sqs_queue.search-api-events_sns-topic-dql_653D177A will be created
  + resource "aws_sqs_queue" "search-api-events_sns-topic-dql_653D177A" {
      + arn                               = (known after apply)
      + content_based_deduplication       = false
      + deduplication_scope               = (known after apply)
      + delay_seconds                     = 0
      + fifo_queue                        = false
      + fifo_throughput_limit             = (known after apply)
      + id                                = (known after apply)
      + kms_data_key_reuse_period_seconds = (known after apply)
      + max_message_size                  = 262144
      + message_retention_seconds         = 345600
      + name                              = "PocketEventBridge-Prod-SNS-SearchApiEvents-Topic-DLQ"
      + name_prefix                       = (known after apply)
      + policy                            = (known after apply)
      + receive_wait_time_seconds         = 0
      + redrive_allow_policy              = (known after apply)
      + redrive_policy                    = (known after apply)
      + sqs_managed_sse_enabled           = (known after apply)
      + tags                              = {
          + "app_code"       = "pocket-content-shared"
          + "component_code" = "pocket-content-shared-pocketeventbridge"
          + "costCenter"     = "Shared"
          + "env_code"       = "prod"
          + "environment"    = "Prod"
          + "owner"          = "Pocket"
          + "service"        = "PocketEventBridge"
        }
      + tags_all                          = {
          + "app_code"       = "pocket-content-shared"
          + "component_code" = "pocket-content-shared-pocketeventbridge"
          + "costCenter"     = "Shared"
          + "env_code"       = "prod"
          + "environment"    = "Prod"
          + "owner"          = "Pocket"
          + "service"        = "PocketEventBridge"
        }
      + url                               = (known after apply)
      + visibility_timeout_seconds        = 30
    }

  # null_resource.search-api-events_null-resource_43620CE7 will be created
  + resource "null_resource" "search-api-events_null-resource_43620CE7" {
      + id = (known after apply)
    }

Plan: 7 to add, 0 to change, 0 to destroy.

Please sign in to comment.