diff --git a/.github/workflows/actions-metrics.yml b/.github/workflows/actions-metrics.yml index 3c8ffe11e8..f1ec78d3fa 100644 --- a/.github/workflows/actions-metrics.yml +++ b/.github/workflows/actions-metrics.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 10 steps: - name: Send GitHub Actions metrics to DataDog - uses: int128/datadog-actions-metrics@56be1c4bf92adece9d10f7fef4ba48bccf8e8c81 # v1.77.0 + uses: int128/datadog-actions-metrics@af1a1a70bb380b079e38053d5fb32ae907ce2b6a # v1.79.0 with: datadog-api-key: ${{ secrets.DATADOG_API_KEY }} collect-job-metrics: true diff --git a/READMES/datasync.md b/READMES/datasync.md new file mode 100644 index 0000000000..82c49f741b --- /dev/null +++ b/READMES/datasync.md @@ -0,0 +1,140 @@ +# Use of AWS Datasync to Facilitate Accelerated Publishing Operations + +The Accelerated Publishing (AP) project has a need to access the Content Management System's (CMS) asset files from a publicly accessible AWS Simple Storage Service (S3) Bucket. The CMS stores assets files which consist of: +images, PDFs, text and other files, on AWS Elastic File System (EFS) filesystems connected to CMS servers. CMS servers store asset files on EFS to facilitate quick deployments, which swap current servers with updated servers, +by mounting and unmounting EFS file systems that effectively acts as persistent file storage not tied to a specific server. To satisfy this need CMS has created infrastructure that utilizes AWS Datasync to transfer files from +EFS to S3 on an automated schedule. This document will describe the architecture, AWS Services, Terraform resources, deployment, and pitfalls. + +# Overview +* [Architecture](#rchitecture) +* [AWS Services and Terraform Resources](aws-services-and-terraform-resources) + * [Datasync](#datasync) + * EFS Location + * S3 Location + * Task + * [EFS](#efs) + * Security group rules + * [S3](#s3) + * Asset files bucket + * Lambda code bucket + * [Identity and Access Management (IAM)](#iam) + * Datasync S3 bucket access role + * Permission Policy + * Lambda function execution role + * Permission Policy + * [Lambda](#lambda) + * Function + * Lambda permission + * [Cloudwatch](#cloudwatch) + * Eventbridge + * Event rule + * Event target +* [Deployment](#deployment) + * Terraform + * Lambda +* [Pitfalls](#pitfalls) + +# Architecture + +![image](https://github.com/department-of-veterans-affairs/va.gov-cms/assets/31904439/9618d869-5485-4547-a9ec-f7d301bbbf91) + +1. Every 5 minutes Cloudwatch Eventbridge triggers the execution of a Lambda function. +2. A Lambda Function containing a simple Python script using AWS API calls to start a Datasync task execution. Lambda requires permission to execute Datasync tasks. +3. The Datasync task is configured with an EFS location which is specifically targeting the CMS EFS filesystem. +4. The Datasync task is configured with an S3 bucket location and moves files from EFS to the bucket. Datasync Tasks require permissions to put objects in the destination bucket. + +# AWS Services and Terraform Resources + +## Datasync + +Datasync is the core of this architecture that facilitates the automated movement of files between EFS and S3. Terraform resources for this come in 3 parts: + +### EFS Location +This requires only three (3) attributes to be defined: +* EFS Mount Target - CMS EFS filesystems have 3 mount targets, one in each VPC subnet. This is simply configured for the mount target in the first subnet. +* Secruity Group ARNs - Security Groups that are associated with the EFS Mount Target. +* Subnet ARN - The ARN of the subnet where the chosen mount target exists. +### S3 Location +This requires only three (3) attributes to be defined: +* S3 Bucket ARN +* Bucket Sub-directory - Or the prefix where files should be copied to. It is currently set to the root. +* Bucket Access Role - IAM role that grants Datasync read and write access to the bucket +### Task +This ties the Datasync locations together and determines which is the source and destination, as well as file transfer settings: +* Destination ARN - CMS Files S3 bucket +* Source ARN - CMS EFS filesystem ARN +* Preserve Deleted Files - Set to remove files from the destination that don't exist in the source. + +## EFS +Datasync uses the EFS filesystem used to persist CMS data between deployments as the file sync source. +### Security group rules +The Datasync EFS location is configured to use the pre-existing EFS SG In the `efs.tf` file. However, the rules needed to be modified to allow inbound port 2049 from the SG itself as well as all egress traffic using itself as a source. + +## S3 +### Asset Files Bucket +The assest files bucket is pre-existing and defiend in `s3.tf` no changes were required for this resource. +### Lambda Code bucket +An additional bucket has been created to faciliate the deployment of the Python script that Lambda uses to start the Datasync Task. Lambda TF resource is configured to source function code from S3. + +## IAM +### Datasync S3 bucket access role +DataSync requires access to your S3 bucket. To do this, DataSync assumes an AWS Identity and Access Management (IAM) role with an IAM policy that determines which actions that the role can perform. +#### Permission Policy +The permission policy for this role was copied from the AWS documenation here: +https://docs.aws.amazon.com/datasync/latest/userguide/create-s3-location.html#create-s3-location-access +### Lambda function execution role +Lambda requires a basic execution role to function properly and additional permissions to interact with Datasync service resources. +#### Permission Policy +In addition to basic execution permissions offered by an AWS managed policy, Lambda also needs to be allowed to start task execution as well as describe EC2 network interfaces. + +## Lambda +Instead of relying on Datasync's built-in schedule feature we use Lambda to start the Datasync Task. This is because Datasync is limited to a minimum schedule frequency of one (1) hour. However, Lambda functions can +be triggered at a much high frequency to meet AP's Datasync requirements. +### Function +As stated previously this a simple Python script that uses the Boto3 library to interact with the AWS API. It takes the Datasync task Amazon Resource Name (ARN) as input into Datasync's `start_task_execution` method. +### Lambda Permission +To allow Cloudwatch Eventbridge to trigger the lambda function it must be given explicit permission to do so. This is not done through IAM resources directly but a `aws_lambda_permission` resource that needs the: +* Action - InvokeFunction +* Function Name +* Principal - Calling service +* Source - Cloudwatch event rule ARN +## Cloudwatch +### Eventbridge +This allows for the triggering of the lambda function and then the Datasync task on a frequency far higher than Datasync itself offers. +#### Event rule +Determines how or when events trigger their defined target. This is set to `rate(5 minutes)` +#### Event target +Determines what gets triggered by the defined rule and simply takes the ARN of the resource that should be triggered. In this case, the Lambda function ARN. + +# Deployment +## Terraform +Checkout the master branch of the [DevOps](https://github.com/department-of-veterans-affairs/devops) repository then browse to the `terraform/environments` folder. CMS.tf and CMS-Test.tf Terraform modules that reference the CMS +Terraform Infrastructure repository are found in the environments `dsva-vagov-staging` and `dsva-vagov-prod`. Incrementing the version number of the `source` attribute to reflect that lastest release from the CMS TF infrastructure repository +will make the resource available to apply to the terraform state of each environment. +## Lambda +While the Python script for the lambda function is tracked in the CMS Terraform Infrastructure repository there exists NO automated deployment of this code to lambda. Rather updates and changes should stay tracked in version +control the script must be packaged in a zip archive and uploaded to the lambda deployment S3 bucket defined in the TF Lambda resource. AWS Provides documenation on how to package Python scripts and their dependencies (boto3) +into a zip archive [here](https://github.com/department-of-veterans-affairs/devops) + +**Abide by these conventions:** + +The python script filename should always be: + +`cms-efs-to-s3-datasync.py` + +The zip archive filename should always be: + +`efs-to-s3-datasync-lambda.zip` + +Depending on application, CMS or CMS-Tes,t the Lambda Deployment archive should be uploaded to: + +* `dsva-vagov-cms-test-lambda` +* `dsva-vagov-cms-lambda` + +Otherwise, update the Terrafrom Lambda resource to reflect any file, archive, or bucket name changes. + +# Pitfalls + +# References +[CMS Terraform Infrastructure Repository](https://github.com/department-of-veterans-affairs/terraform-aws-vsp-cms) +[Original Issue](https://github.com/department-of-veterans-affairs/va.gov-cms/issues/16925) diff --git a/composer.json b/composer.json index 345cf9e0ae..9e9478136f 100644 --- a/composer.json +++ b/composer.json @@ -221,7 +221,7 @@ "symfony/phpunit-bridge": "^5.1", "symfony/process": "^6.3", "symfony/routing": "^6.3", - "va-gov/content-build": "^0.0.3436", + "va-gov/content-build": "^0.0.3437", "vlucas/phpdotenv": "^5.3", "webflo/drupal-finder": "^1.0.0", "webmozart/path-util": "^2.3", diff --git a/composer.lock b/composer.lock index f724c040e9..c71d3e0498 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "95c4c7cc2bce561c6d11a295eccc567b", + "content-hash": "5a2e47403b810bc18a1b3c3793555a6a", "packages": [ { "name": "asm89/stack-cors", @@ -2016,16 +2016,16 @@ }, { "name": "doctrine/lexer", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", "shasum": "" }, "require": { @@ -2033,11 +2033,11 @@ "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", + "doctrine/coding-standard": "^9 || ^12", "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" + "vimeo/psalm": "^4.11 || ^5.21" }, "type": "library", "autoload": { @@ -2074,7 +2074,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.0" + "source": "https://github.com/doctrine/lexer/tree/2.1.1" }, "funding": [ { @@ -2090,7 +2090,7 @@ "type": "tidelift" } ], - "time": "2022-12-14T08:49:07+00:00" + "time": "2024-02-05T11:35:39+00:00" }, { "name": "doctrine/persistence", @@ -6519,17 +6519,17 @@ }, { "name": "drupal/geocoder", - "version": "4.22.0", + "version": "4.23.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/geocoder.git", - "reference": "8.x-4.22" + "reference": "8.x-4.23" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/geocoder-8.x-4.22.zip", - "reference": "8.x-4.22", - "shasum": "cc6d9bbb542c5073e4a19dececa58ffdac146aa4" + "url": "https://ftp.drupal.org/files/projects/geocoder-8.x-4.23.zip", + "reference": "8.x-4.23", + "shasum": "111a67f6da761848882482041a7c67c797e96a3e" }, "require": { "davedevelopment/stiphle": "^0.9.2", @@ -6573,8 +6573,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-4.22", - "datestamp": "1706827890", + "version": "8.x-4.23", + "datestamp": "1707161636", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -9733,17 +9733,17 @@ }, { "name": "drupal/migrate_tools", - "version": "6.0.2", + "version": "6.0.4", "source": { "type": "git", "url": "https://git.drupalcode.org/project/migrate_tools.git", - "reference": "6.0.2" + "reference": "6.0.4" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/migrate_tools-6.0.2.zip", - "reference": "6.0.2", - "shasum": "9d6346306e52a6741f3eb52e32caaa95a2752ad0" + "url": "https://ftp.drupal.org/files/projects/migrate_tools-6.0.4.zip", + "reference": "6.0.4", + "shasum": "63c571aefece96b199ce8b8f90da648186502be4" }, "require": { "drupal/core": ">=9.1", @@ -9761,8 +9761,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "6.0.2", - "datestamp": "1694604959", + "version": "6.0.4", + "datestamp": "1707330330", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -10998,17 +10998,17 @@ }, { "name": "drupal/path_redirect_import", - "version": "2.0.8", + "version": "2.0.9", "source": { "type": "git", "url": "https://git.drupalcode.org/project/path_redirect_import.git", - "reference": "2.0.8" + "reference": "2.0.9" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/path_redirect_import-2.0.8.zip", - "reference": "2.0.8", - "shasum": "a1fb1fe44bf657228d099576874ade5e6065c414" + "url": "https://ftp.drupal.org/files/projects/path_redirect_import-2.0.9.zip", + "reference": "2.0.9", + "shasum": "c33d0f9cb1323bdf28432ba67af750f324a2d4c9" }, "require": { "drupal/core": ">=9.1", @@ -11023,8 +11023,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "2.0.8", - "datestamp": "1701853731", + "version": "2.0.9", + "datestamp": "1707311721", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -24147,16 +24147,16 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", "shasum": "" }, "require": { @@ -24164,9 +24164,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -24203,7 +24200,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" }, "funding": [ { @@ -24219,20 +24216,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -24240,9 +24237,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -24286,7 +24280,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -24302,20 +24296,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", - "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", "shasum": "" }, "require": { @@ -24323,9 +24317,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -24365,7 +24356,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" }, "funding": [ { @@ -24381,7 +24372,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php83", @@ -25816,16 +25807,16 @@ }, { "name": "va-gov/content-build", - "version": "v0.0.3436", + "version": "v0.0.3437", "source": { "type": "git", "url": "https://github.com/department-of-veterans-affairs/content-build.git", - "reference": "cd8386ca3214ee3bdc04f635605db34421c96058" + "reference": "7e1bb61b6bb656124410196ee2bb9933417c8db5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/department-of-veterans-affairs/content-build/zipball/cd8386ca3214ee3bdc04f635605db34421c96058", - "reference": "cd8386ca3214ee3bdc04f635605db34421c96058", + "url": "https://api.github.com/repos/department-of-veterans-affairs/content-build/zipball/7e1bb61b6bb656124410196ee2bb9933417c8db5", + "reference": "7e1bb61b6bb656124410196ee2bb9933417c8db5", "shasum": "" }, "type": "node-project", @@ -25852,9 +25843,9 @@ "description": "Front-end for VA.gov. This repository contains the code that generates the www.va.gov website. It contains a Metalsmith static site builder that uses a Drupal CMS for content. This file is here to publish releases to https://packagist.org/packages/va-gov/content-build, so that the CMS CI system can install it and update it using standard composer processes, and so that we can run tests across both systems. See https://github.com/department-of-veterans-affairs/va.gov-cms for the CMS repo, and stand by for more documentation.", "support": { "issues": "https://github.com/department-of-veterans-affairs/content-build/issues", - "source": "https://github.com/department-of-veterans-affairs/content-build/tree/v0.0.3436" + "source": "https://github.com/department-of-veterans-affairs/content-build/tree/v0.0.3437" }, - "time": "2024-02-06T17:04:05+00:00" + "time": "2024-02-06T20:33:35+00:00" }, { "name": "vlucas/phpdotenv", diff --git a/tests/cypress/integration/features/content_type/facilities/vba/vba_facility.feature b/tests/cypress/integration/features/content_type/facilities/vba/vba_facility.feature index 375e546b2f..c464663a9c 100644 --- a/tests/cypress/integration/features/content_type/facilities/vba/vba_facility.feature +++ b/tests/cypress/integration/features/content_type/facilities/vba/vba_facility.feature @@ -13,6 +13,37 @@ Feature: CMS User may effectively interact with the VBA Facility form Then the primary tab "View" should exist Then the primary tab "Edit" should not exist + Scenario: Test restricted_archive workflow prevents archiving a VBA Facility as a VBA editor. + When I am logged in as a user with the roles "content_creator_vba, content_publisher" + And my workbench access sections are set to "1065" + When I am at "/node/4063/edit" + And I scroll to element "select#edit-moderation-state-0-state" + Then an option with the text "Archived" from dropdown with selector "select#edit-moderation-state-0-state" should not be visible + And I scroll to position "bottom" + And I click the "Unlock" link + And I click the "Confirm break lock" button + + Scenario: Test restricted_archive workflow allows archiving a VBA Facility as a content_admin. + Given I am logged in as a user with the "content_admin" role + # Columbia VA Regional Benefit Office + When I am at "/node/4063/edit" + And I scroll to element "select#edit-moderation-state-0-state" + Then an option with the text "Archived" from dropdown with selector "select#edit-moderation-state-0-state" should be visible + + When I select option "Archived" from dropdown with selector "select#edit-moderation-state-0-state" + And I fill in field with selector "#edit-revision-log-0-value" with value "[Test Data] Revision log message." + And I save the node + Then I should see "has been updated." + + When I click the edit tab + And I scroll to element "select#edit-moderation-state-0-state" + Then an option with the text "Published" from dropdown with selector "select#edit-moderation-state-0-state" should be visible + + When I select option "Published" from dropdown with selector "select#edit-moderation-state-0-state" + And I fill in field with selector "#edit-revision-log-0-value" with value "[Test Data] Revision log message." + And I save the node + Then I should see "has been updated." + Scenario: Enable banner segment and ensure expected fields are present Given I am logged in as a user with the "content_admin" role And my workbench access sections are set to "1065"