From 66d59c886ca1581617420bedfb64d8be498c42c2 Mon Sep 17 00:00:00 2001 From: Christopher Groskopf Date: Tue, 12 May 2026 10:55:43 -0700 Subject: [PATCH 1/2] Migrate deployment from pid-env. Remove ecr stack. --- deployment/app_config/app.yml | 30 ++ deployment/app_config/db.yml | 8 + deployment/app_config/html.yml | 280 ++++++++++++ deployment/app_config/message.yml | 108 +++++ deployment/app_config/security.yml | 50 +++ deployment/config/config.yaml | 3 + deployment/config/dev/config.yaml | 2 + deployment/config/dev/redis-stack.yaml | 10 + deployment/config/dev/service-stack.yaml | 32 ++ deployment/config/prd/config.yaml | 2 + deployment/config/prd/redis-stack.yaml | 10 + deployment/config/prd/service-stack.yaml | 31 ++ deployment/environment_config/dev_config.sh | 4 + deployment/environment_config/prd_config.sh | 4 + deployment/scripts/ecr_push.sh | 54 +++ deployment/scripts/ecs_restart.sh | 45 ++ deployment/templates/redis-stack.yaml | 54 +++ deployment/templates/service-stack.yaml.j2 | 456 ++++++++++++++++++++ pyproject.toml | 14 + uv.lock | 408 ++++++++++++++++++ 20 files changed, 1605 insertions(+) create mode 100644 deployment/app_config/app.yml create mode 100644 deployment/app_config/db.yml create mode 100644 deployment/app_config/html.yml create mode 100644 deployment/app_config/message.yml create mode 100644 deployment/app_config/security.yml create mode 100644 deployment/config/config.yaml create mode 100644 deployment/config/dev/config.yaml create mode 100644 deployment/config/dev/redis-stack.yaml create mode 100644 deployment/config/dev/service-stack.yaml create mode 100644 deployment/config/prd/config.yaml create mode 100644 deployment/config/prd/redis-stack.yaml create mode 100644 deployment/config/prd/service-stack.yaml create mode 100644 deployment/environment_config/dev_config.sh create mode 100644 deployment/environment_config/prd_config.sh create mode 100755 deployment/scripts/ecr_push.sh create mode 100755 deployment/scripts/ecs_restart.sh create mode 100644 deployment/templates/redis-stack.yaml create mode 100644 deployment/templates/service-stack.yaml.j2 create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/deployment/app_config/app.yml b/deployment/app_config/app.yml new file mode 100644 index 0000000..8fdb28f --- /dev/null +++ b/deployment/app_config/app.yml @@ -0,0 +1,30 @@ +# Used for the title and header on layout.erb +organization_name: 'California Digital Library' +application_name: 'PID Service' + +# The server and port where the main PID application will be hosted (comment out the port if you do not need to specify) +app_host: <%= ENV['APP_HOST'] %> +app_port: <%= ENV['APP_PORT'] %> + +# The url that the system should direct users to when an inactive PID is requested +dead_pid_url: <%= ENV['DEAD_PID_URL'] %> + +# Redis host and port are required +redis_host: <%= ENV['REDIS_HOST'] %> +redis_port: <%= ENV['REDIS_PORT'] %> +redis_use_ssl: <%= ENV['REDIS_USE_SSL'] %> + +# Limit the number of results that are returned during a PID search +search_results_limit: 50 + +# The maximum CSV file size allowed for upload on the Edit PID page (in kb) +max_upload_csv_size: 10240 #10MB + +# The email address that the system uses to send mail (SMTP) +email_sender_address: <%= ENV['SMTP_SENDER_ADDRESS'] %> + +# Smtp settings if we're using that +smtp_host: <%= ENV['SMTP_HOST'] %> +smtp_port: <%= ENV['SMTP_PORT'] %> +smtp_username: <%= ENV['SMTP_USERNAME'] %> +smtp_password: <%= ENV['SMTP_PASSWORD'] %> \ No newline at end of file diff --git a/deployment/app_config/db.yml b/deployment/app_config/db.yml new file mode 100644 index 0000000..7f5a3c7 --- /dev/null +++ b/deployment/app_config/db.yml @@ -0,0 +1,8 @@ +# DB connection settings +db_adapter: 'mysql2' +db_encoding: 'utf8' +db_host: <%= ENV['DB_HOST'] %> +db_port: 3306 +db_name: <%= ENV['DB_NAME'] %> +db_username: <%= ENV['DB_USERNAME'] %> +db_password: <%= ENV['DB_PASSWORD'] %> \ No newline at end of file diff --git a/deployment/app_config/html.yml b/deployment/app_config/html.yml new file mode 100644 index 0000000..1338c25 --- /dev/null +++ b/deployment/app_config/html.yml @@ -0,0 +1,280 @@ +# Navigation bar links +nav_admin: 'Administration' +nav_home: 'Home' +nav_login: 'Login (for PID account users)' +nav_logout: 'Logout' +nav_pid_search: 'Find PIDs' +nav_pid_update: 'Batch PIDs' +nav_pid_create: 'Mint PIDs' +nav_public_search: 'Find PIDs' +nav_reports: 'Reports' +nav_user: 'Profile' + +# Page/Section headers +header_admin: 'Administration' +header_recovery: 'Account Recovery' +header_group_create: 'New group' +header_group_list: 'Groups' +header_group_view: 'Group profile' +header_index: 'Home' +header_login: 'Login' +header_manage_maintainers: 'Users who can manage this group' +header_manage_users: 'Users that belong to this group' +header_not_found: 'The page you requested could not be found' +header_pid_edit: 'Batch modify PIDs' +header_pid_history: 'History' +header_pid_inactive: 'Inactive!' +header_pid_register: 'Mint a new PID' +header_pid_register_dead_url: 'The following PID(s) were created but the URL seems to be returning a 300 level - Moved, 404 - Page not Found, or a 500 - Server Error!
Please double check the URL to make sure its correct.' +header_pid_register_duplicate_url: 'The following PID(s) are already defined for the following URL(s).
You will be notified via email if they change.' +header_pid_register_success: 'Successfully created the following PID(s)' +header_pid_view: 'PID:' +header_reports: 'PID maintenance reports' +header_report_duplicate: 'Duplicate URLs' +header_report_inactive_criteria: 'Find inactive PIDs for a specific group' +header_report_inactive_results: 'Inactive PIDs' +header_report_invalid: 'PIDs with an invalid URL' +header_report_invalid_error: 'The following URLs are returning a server error' +header_report_invalid_moved: 'The following URLs are working but are redirected to another URL which may or may not be correct' +header_report_invalid_not_found: 'The following URLs are returning a not found message' +header_report_invalid_skipped: 'The following domains cannot be scanned due to contractual agreements established with the hosting company' +header_report_maintenance_criteria: 'PID maintenance criteria' +header_report_maintenance_results: 'PID maintenance results' +header_report_modifications_criteria: 'Modification details criteria' +header_report_modifications_results: 'Modification details results' +header_search_criteria: 'Or use the following search criteria' +header_search_pid_set: 'Find an individual PID or specific group of PIDs' +header_search_results: 'Search results - click on the PID number to view or update' +header_search_interested: 'PIDs managed by another group' +header_skip_check_add: 'Add a new domain' +header_skip_check_list: 'Domains that we cannot run link validation against' +header_unauthorized: 'You do not permission to do that' +header_user_list: 'Users' +header_user_register: 'New user registration' +header_user_view: 'User profile' +header_user_view_password: 'Change your password' + +# Page based links to other pages +link_to_admin: 'Return to the administration page' +link_to_create_group: 'Create a new group' +link_to_delete_group: 'Delete' +link_to_recovery: 'Recover my account' +link_to_group_list: 'Return to the group list' +link_to_login: 'Login' +link_to_register_user: 'Register a new user' +link_to_reports: 'Return to the report list' +link_to_user_list: 'Return to the user list' + +# Form elements +form_active: 'Is Active?' +form_activity_date_range: 'Activity Date Range:' +form_admin: 'Is system admin?' +form_affiliation: 'Affiliation:' +form_change_category: 'Change Category:' +form_confirm_password: 'Confirm:' +form_created_by: 'Created by:' +form_created_date: 'Created Date:' +form_created_date_range: 'Created Date Range:' +form_date_range: 'Date Range:' +form_deactivated_since: 'Deactivated From:' +form_description: 'Description:' +form_domain: 'Domain' +form_email: 'Email:' +form_file: 'CSV File:' +form_group: 'Group:' +form_group_maintainer: 'Can maintain group?' +form_group_name: 'Name:' +form_groupid: 'Group Id:' +form_interested: 'Groups that are watching this PID:' +form_interesteds: 'Show only the PIDs you are watching?' +form_locked: 'Is Locked?' +form_maintainers: 'Managers:' +form_modified_by: 'Last Modified by:' +form_modified_date: 'Last Modified:' +form_modified_date_range: 'Last Modified Range:' +form_name: 'Full Name:' +form_new_password: 'New Password:' +form_old_url: 'Old URL:' +form_password: 'Password:' +form_pid: 'PID:' +form_pid_range: 'PID Range:' +form_pid_set: 'PIDs (one per line):' +form_url: 'URL:' +form_urls: 'URL(s):' +form_userid: 'User Id:' +form_users: 'Users:' +form_notes: 'Notes:' +form_readonly: 'Read Only Account?' + +# Hints that appear next to the criteria on the search and stats report pages +form_date_format: '(Please follow the hint as date format)' +form_search_and: 'and' +form_search_between: 'between' +form_search_url_instruction: 'You may enter partial urls (e.g. google.com/)' + +# No matches to the search +search_no_matches: 'No PIDs matched your search criteria!' +unable_to_load_criteria_defaults: 'Unable to retrieve the defaults for the criteria' + +# Report errors +report_no_activity: 'No activity to report.' + +# Create pid textarea tooltip +create_pids_tooltip: 'One URL per line. Each URL should include the protocol (e.g. http://)' +pid_set_tooltip: 'Your PIDs should be separated by commas.' + +# Batch processing +batch_duplicate_url_log: 'The following URL(s) are already in use by the following PID(s)' +batch_failures: 'The following PID(s) could not be updated:' +batch_mint_log: 'Successfully minted the following PID(s):' +batch_revision_log: 'Successfully updated the following PID(s):' +batch_upload_failure: 'Unable to process the file you selected. Make sure its a CSV file!' + +# Create multiple pids, some failed error message +create_pids_some_errors: + "The URLs above could not be validated!
+ Please ensure that you have included the protocol (e.g. http://), and that each URL appears on its own line before + resubmitting the form." + +# Buttons +button_add_skip_check: 'Add' +button_create_group: 'Save' +button_csv_download: 'Download as CSV' +button_recovery: 'Send recovery email' +button_login: 'Login' +button_process_batch: 'Process File' +button_register_user: 'Save' +button_reset: 'Reset' +button_reset_password: 'Save' +button_save_group: 'Save' +button_save_user: 'Save' +button_search: 'Search' +button_update_pid: 'Save' +button_file_upload: 'Choose File' + +# Table headings +th_active: 'Active?' +th_affiliation: 'Affiliation' +th_change_category: 'Change Category' +th_change_types: 'Changes' +th_created_on: 'Created On' +th_deactivated_by: 'Deactivated By' +th_deactivated_on: 'Deactivated On' +th_description: 'Description' +th_domain: 'Domain' +th_duplicate_pids: 'Duplicate PIDs' +th_email: 'Email' +th_group: 'Group' +th_group_name: 'Name' +th_groupid: 'Group Id' +th_last_activity: 'Last Activity On' +th_last_modified_by: 'Last Modified By' +th_last_modified_on: 'Last Modified On' +th_locked: 'Locked?' +th_maintainer: 'Manager For' +th_maintainers: '# of Managers' +th_modified_date: 'Updated' +th_name: 'Full Name' +th_notes: 'Notes' +th_number_created: 'Created' +th_number_deactivated: 'Deactivated' +th_number_modified: 'Modified' +th_number_modifications: '# Times Modified' +th_old_url: 'Prior URL' +th_pid: 'PID' +th_url: 'URL' +th_userid: 'User Id' +th_users: '# of Users' + +# True/False values in tables +td_false: 'No' +td_true: 'Yes' + +# Make sure that you do not use tab characters in the text values below. It will cause the parser to crash +admin_text: + "Please select from the options below: + " + +admin_text_super: + "Please select from the options below: + " + +dead_pid_text: + "

The resource you requested is no longer available.

+ The issuing agency has either removed the resource from the Web or has moved it without providing a forwarding address.

+ If you know the current location for the resource or have questions, please contact Lam Pham" + +edit_pid_duplicate_url: 'This URL is also assigned to PID(s) {others} - (last checked on {last_checked})' + +edit_pid_interested_parties: "The interested groups listed above will be notified of any changes you make to this PID." + +edit_pid_invalid_url_300: 'Warning: This URL returned a "moved or redirect" message (HTTP status: {status}) on {last_checked}' +edit_pid_invalid_url_400: 'Warning: This URL returned a "not found" message (HTTP status: {status}) on {last_checked}' +edit_pid_invalid_url_500: 'Warning: This URL returned a "server error" message (HTTP status: {status}) on {last_checked}' + +edit_pid_not_owner: "You are not the owner of this PID and cannot make changes!

If you think it should be updated please contact: {?}." + +edit_pid_text: + "Select the CSV file that contains your changes. The file should be in the following format (without headers!):

+   PID Id, New URL, Notes


+ To edit a PID you would include both the PID Id and the new URL
+   1234,http://www.newsite.org/pageA,Sample Edit PID

+ To deactivate a PID you would leave its URL blank. (This will not actually delete the URL, just deactivate the PID)
+   1234,,Sample Deactivation

" + +edit_pid_text_2: + "- The PID Id column should only contain the PID's number, not the entire URL. (e.g. 1234 not /PID/1234)

+ - You may mix edit, mint, and deactivate records within the same file.

+ - There is a 20MB limit to the size of the CSV file. Large files may take a long time to finish processing!" + +error_text: + "Oops, something went wrong! Please use your browser's back button and try that again." + +groups_text: "Groups can be deleted once all of their their users and managers have been removed." + +index_text: + "This resolution service allows UC Libraries to create a persistent identifier (PID) for any URL they choose. + The Shared Cataloging Program (SCP) creates and + maintains PIDs for CDL-licensed content.

This service uses a modified version of OCLC's PURL software. For more information on + PURLs and the PURL software, see PURL Frequently Asked Questions. +

Refer to the FAQ for answers to your questions.

If the FAQ does not address + your specific issue, please email CDL-D2D-TECH-L@ucop.edu. Include a detailed + description of your concern or the error you are encountering. If at all possible also include a screenshot of the page." + +new_group_text: "You will be able to add users and maintainers to the group once you click the 'Save' button." + +new_pids_text: + "Enter your new URL(s) in the box above. Every URL should appear on a separate line and should include + a protocol (e.g. http://)." + +not_found_text: 'The page you requested could not be found.' + +report_modifications_text: "Click on the PID number to view the modification details.

The CSV download displays the current and prior URL along with the user id.
" + +reports_text: + "Please select on of the following reports below: + " + +search_text: 'You may click on any of the column headers below to sort the results.' + +unauthorized_text: + "You don't have the necessary permission to perform that action.

Please speak with your local administrator if you feel that + this is a mistake." + +user_profile_text: + "If you do not want to change your password, leave those fields blank." + +recovery_text: + "Reset your Password or retrieve your User Id for login" \ No newline at end of file diff --git a/deployment/app_config/message.yml b/deployment/app_config/message.yml new file mode 100644 index 0000000..5c0b7cc --- /dev/null +++ b/deployment/app_config/message.yml @@ -0,0 +1,108 @@ +# Authorization messages +account_inactive: 'This account is no longer active.' +account_locked: 'Your account has been locked due to too many failed login attempts!
Your account will automatically unlock after ${?} minutes.' +failed_login: 'Invalid user id or password.' +failed_login_close_to_lockout: 'Invalid password! You have #{?} attempts left before your account is automatically locked.' +login: 'Welcome back #{?}!' +logout: 'You have been logged out of the system.' +session_expired: 'Your session has expired. Please log back in to continue working.' + +# General form validation messages +invalid_email: 'That email is not in the system!' +invalid_login: 'That user id is not in the system!' +login_already_exists: 'That user id is already in use!' +password_mismatch: 'The passwords you entered did not match!' +no_email: 'The email address cannot be blank!' +no_login: 'The user id cannot be blank!' +no_password: 'The password cannot be blank!' +bad_reset_email: 'The email you entered is not valid!' + +# User controller messages +user_not_found: "Please use your browser's back button to return to the previous page." +user_password_reset_email: 'If the email address you entered is in our system, we will send you a link to reset your password.' +user_password_reset_expired: + 'Your password reset request has expired. For security reasons you have only #{?} minutes between + the time you request a reset and you follow the link to the reset form in the confirmation email.' +user_password_reset_success: 'Your password has been reset.' +user_password_reset_unauthorized: 'You do not have permission to reset this password.' +user_register_failure: 'Unable to register the new user!' +user_register_invalid_group: 'You do not manage that group!' +user_register_success: 'The user is now registered.' +user_unauthorized: 'You do not have permission to view or change that user!' +user_update_failure: 'Unable to save your changes!' +user_update_success: 'Your changes have been saved.' +user_password_forgot_max_attempts: 'Too many requests have been made to reset your password. Please contact your administrator.' + +# Group controller messages +group_add_maintainer_failure: 'Unable to add the user!' +group_add_maintainer_duplicate: 'That user is already a maintainer of that group!' +group_add_maintainer_success: 'Successfully added the user.' +group_add_user_failure: 'Unable to add the user!' +group_add_user_duplicate: 'That user is already a member of that group!' +group_add_user_success: 'Successfully added the user.' +group_create_duplicate: 'That group already exists!' +group_create_failure: 'Unable to save your changes!' +group_create_success: 'The group has been saved.' +group_delete_failure: 'Unable to delete the group!' +group_delete_has_children: 'You must first remove all of the users and maintainers from the group!' +group_delete_success: 'The group has been deleted.' +group_not_found: 'That group could not be found!' +group_remove_maintainer_failure: 'Unable to remove the user!' +group_remove_maintainer_missing: 'That user is not a maintainer of the group!' +group_remove_maintainer_self: 'You cannot remove yourself!' +group_remove_maintainer_success: 'Sucessfully removed the user.' +group_remove_user_failure: 'Unable to remove the user!' +group_remove_user_missing: 'That user is not a member of the group!' +group_remove_user_success: 'Successfully removed the user.' +group_unauthorized: 'You do not have permission to manage that group!' +group_update_failure: 'Unable to save your changes!' +group_update_success: 'The group has been saved.' + +# PID controller messages +pid_duplicate_url: 'The URL you specified is already used by PID {?}.
You are now registered as an interested party for that PID.
You will be notified of any future changes made to the PID.' +pid_duplicate_url_warn: 'The URL you specified is also used by PID(s) {?}.
Your changes have been saved but you may want to consider using the other PID.' +pid_mint_dead_url: 'The URL for PID {?} is not active. The PID was created but you should verify that you have supplied the correct URL!' +pid_mint_default_note: 'Incoming request from {?ip?} to mint {?}' +pid_mint_empty_url: 'You must provide a URL!' +pid_mint_failure: 'Unable to create PID for URL {?}.' +pid_mint_invalid_url: 'Invalid URL format.' +pid_not_found: 'That PID could not be found!' +pid_revise_dead_url: 'The URL you specified is returning an HTTP {?} status code!
Your change has been saved but please make sure you have the correct URL!' +pid_search_not_enough_criteria: 'Your criteria is too broad! Please narrow your search!' +pid_search_not_found: 'Your criteria returned no results!' +pid_unauthorized: 'You do not have permission to modify this PID!' +pid_update_failure: 'Unable to save your changes.' +pid_update_invalid_url: 'Invalid URL format' +pid_update_success: 'Your changes have been saved.' + +# Interested party +# Use {?} to have the system add the show PID url in the message +notify_interested_change: 'PID {?} has been modified:' +notify_interested_deactivation: 'The PID has been deactivated!' +# Use {?} to have the system add the PID id in the subject +notify_interested_subject: 'A PID you watch has been modified! (PID - {?})' +# Use {?old?} to have the system add the old URL and {?new?} for the new URL into the message +notify_interested_url_change: 'The URL has changed from {?old?} to {?new?}.' + +# Batch processing +batch_process_failure: 'All or part of your CSV file could not be processed. Please see the errors below for details.' +batch_process_mint_failure: 'PID {?} could not be minted - ' +batch_process_mint_inactive: 'Cannot mint a new inactive PID! Make sure that the URL column has a value if the PID id is empty!' +batch_process_mint_invalid: 'PID {?} could not be minted because the url is invalid!' +batch_process_revise_failure: 'PID {?} could not be updated - ' +batch_process_revise_invalid: 'PID {?} could not be edited because the url is invalid!' +batch_process_revise_missing: 'PID {?} does not exist!' +batch_process_revise_wrong_group: 'PID {?} does not belong to your group!' +batch_process_success: 'Your CSV was successfully processed!
Click on a PID Number below to view the PID.' +invalid_file_type: 'The file you upload must be in the CSV format!' +no_file_selected: 'You must select a file to upload!' + +reports_failure: 'There was a problem processing the report: ' + +# Domains that cannot be checked +skip_delete: 'The domain has been deleted' +skip_duplicate: 'The domain you specified is already in the system!' +skip_failure: 'Unable to add the specified domain!' +skip_not_found: 'That domain could not be found!' +skip_success: 'The domain has been added to the list.' +skip_unauthorized: "You cannot delete that domain because you do not belong to that group." \ No newline at end of file diff --git a/deployment/app_config/security.yml b/deployment/app_config/security.yml new file mode 100644 index 0000000..577e983 --- /dev/null +++ b/deployment/app_config/security.yml @@ -0,0 +1,50 @@ +# Session secret +session_secret: <%= ENV['SESSION_SECRET'] %> + +# Session expiration in seconds +session_expires: 14400 + +# Default system administrator (the system will create the specified group if specified and it does not already exist) +create_default_admin: false # Indicates whether or not the admin account gets created on startup +default_group_id: 'ADM' +default_group_name: 'Administrators' +default_admin_login: 'admin' # CHANGE THIS!!! +default_admin_password: 'password' # CHANGE THIS!!! +default_admin_name: 'Administrator' +default_admin_email: 'admin@institution.org' + +# The number of failed login attempts before the user's account is locked +max_login_attempts: 5 +# The number of minutes after which an account will unlock automatically - leave blank to prevent accounts from auto-unlocking +release_account_lock_after: 10 + +# Specify the length in minutes that a password reset request will remain active. The time should allow for the user to receive their email +# from the system with the link to their reset page +password_reset_timeout: 15 + +# Specify the password reset email parameters. The system will replace the following values if included in the body: +# {?name?} <= user.name +# {?url?} <= the URL to the user's reset page (You MUST include this one) +# {?affiliation?} <= the user.affiliation +# {?group?} <= the user.group.name +# {?timeframe?} <= the timeout specified above +password_reset_email_subject: 'UCOP PID Service - Recovery Email' +password_reset_email_body: "Hello {?name?},\n\nYou requested a recovery email for the UCOP PID Service.\n\nTo access the recovery page to reset your password or retrieve your User Id for login, please follow the link: {?url?}\n\nFor your security, this reset request will expire in {?timeframe?} minutes. If you did not request a password reset, please ignore this message." + +# Specify the accounts, subject, and message that should be sent to the administrators when an account gets locked +# The system will replace the following placeholders +# {?login?} <= user's login id +# {?name?} <= user's name +# {?email?} <= user's email address +# {?ip?} <= the ip address that generated the lockout +account_lock_email_to: <%= ENV['ACCOUNT_LOCK_EMAIL_TO'] %> +account_lock_email_subject: 'UCOP PID Service - Account Lockout' +account_lock_email_body: + "The following account has been locked due to too many failed login attempts.\n\n + Login: {?login?}\n + Name: {?name?}\n + Source IP: {?ip?}" + +# The pages that the user should be sent to after the specified action completes successfully. E.g. go to the /link/index page after login +target_after_login: '/link' +target_after_logout: '/user/login' \ No newline at end of file diff --git a/deployment/config/config.yaml b/deployment/config/config.yaml new file mode 100644 index 0000000..2d712a5 --- /dev/null +++ b/deployment/config/config.yaml @@ -0,0 +1,3 @@ +project_code: pidservice +region: us-west-2 +service_name: pidservice \ No newline at end of file diff --git a/deployment/config/dev/config.yaml b/deployment/config/dev/config.yaml new file mode 100644 index 0000000..0390fad --- /dev/null +++ b/deployment/config/dev/config.yaml @@ -0,0 +1,2 @@ +profile: cdl-d2d-dev +environment: dev \ No newline at end of file diff --git a/deployment/config/dev/redis-stack.yaml b/deployment/config/dev/redis-stack.yaml new file mode 100644 index 0000000..35bb0b4 --- /dev/null +++ b/deployment/config/dev/redis-stack.yaml @@ -0,0 +1,10 @@ +template: + path: redis-stack.yaml + type: file +parameters: + ServiceName: {{ service_name }} + Environment: {{ environment }} + VPCID: vpc-0e484a5e57340c43d # cdl-d2d-dev-vpc + SubnetIDs: + - subnet-0e10823bc21309b7f # cdl-d2d-dev-public-2a + - subnet-09dfebb2a40cbf93a # cdl-d2d-dev-public-2b \ No newline at end of file diff --git a/deployment/config/dev/service-stack.yaml b/deployment/config/dev/service-stack.yaml new file mode 100644 index 0000000..c578e59 --- /dev/null +++ b/deployment/config/dev/service-stack.yaml @@ -0,0 +1,32 @@ +template: + path: service-stack.yaml.j2 + type: file +dependencies: + - dev/redis-stack.yaml +parameters: + ServiceName: {{ service_name }} + Environment: {{ environment }} + MainSiteName: pidservice.d2ddev.cdlib.net + ContainerPort: "80" + LoadBalancerSecurityGroupSources: + - !ssm /d2d/dev/ucop-vpn-prefix-list-id + - !ssm /d2d/dev/cdl-eip-prefix-list-id + DeadPIDURL: https://pidservice.d2ddev.cdlib.net/link/inactive + AccountLockEmailTo: lam.pham@ucop.edu + SMTPSenderAddress: pid-no-reply@cdlib.org + RedisCacheSecurityGroupID: !stack_output dev/redis-stack.yaml::RedisCacheSecurityGroupID + RedisCacheAddress: !stack_output dev/redis-stack.yaml::RedisCacheAddress + RedisCachePort: !stack_output dev/redis-stack.yaml::RedisCachePort + RedisCacheUseSSL: "true" + VPCID: vpc-0e484a5e57340c43d # cdl-d2d-dev-vpc + SubnetIDs: + - subnet-0e10823bc21309b7f # cdl-d2d-dev-public-2a + - subnet-09dfebb2a40cbf93a # cdl-d2d-dev-public-2b + RDSSecurityGroupID: sg-09025405ae7b4a5ad # d2d-rds-sg (dev) + CertificateARN: arn:aws:acm:us-west-2:445017934155:certificate/0b7856d1-5261-4b72-93df-b3e7366f1462 + Route53ZoneID: Z0067904Z8818VNHPHA4 + RDSCredentialsSecretARN: arn:aws:secretsmanager:us-west-2:445017934155:secret:dev/PIDService/MySQL-isLMFA + SMTPCredentialsSecretARN: arn:aws:secretsmanager:us-west-2:445017934155:secret:dev/SES/SMTP-v52jns + SessionSecretARN: arn:aws:secretsmanager:us-west-2:445017934155:secret:dev/PIDService/SessionSecret-yfn4Wq +sceptre_user_data: + load_balancer_security_group_sources: !stack_attr parameters.LoadBalancerSecurityGroupSources diff --git a/deployment/config/prd/config.yaml b/deployment/config/prd/config.yaml new file mode 100644 index 0000000..a00eabe --- /dev/null +++ b/deployment/config/prd/config.yaml @@ -0,0 +1,2 @@ +profile: cdl-d2d-prd +environment: prd \ No newline at end of file diff --git a/deployment/config/prd/redis-stack.yaml b/deployment/config/prd/redis-stack.yaml new file mode 100644 index 0000000..77daf85 --- /dev/null +++ b/deployment/config/prd/redis-stack.yaml @@ -0,0 +1,10 @@ +template: + path: redis-stack.yaml + type: file +parameters: + ServiceName: {{ service_name }} + Environment: {{ environment }} + VPCID: vpc-0e8902cf873551dcb # cdl-d2d-prd-vpc + SubnetIDs: + - subnet-09915dc8f8bd30423 # cdl-d2d-prd-public-2a + - subnet-06483f1e8e186fbca # cdl-d2d-prd-public-2b \ No newline at end of file diff --git a/deployment/config/prd/service-stack.yaml b/deployment/config/prd/service-stack.yaml new file mode 100644 index 0000000..2b85c5f --- /dev/null +++ b/deployment/config/prd/service-stack.yaml @@ -0,0 +1,31 @@ +template: + path: service-stack.yaml.j2 + type: file +dependencies: + - prd/redis-stack.yaml +parameters: + ServiceName: {{ service_name }} + Environment: {{ environment }} + MainSiteName: pidservice.d2dprd.cdlib.net + ContainerPort: "80" + LoadBalancerSecurityGroupSources: + - 0.0.0.0/0 + DeadPIDURL: https://pidservice.d2dprd.cdlib.net/link/inactive + AccountLockEmailTo: lam.pham@ucop.edu + SMTPSenderAddress: pid-no-reply@cdlib.org + RedisCacheSecurityGroupID: !stack_output prd/redis-stack.yaml::RedisCacheSecurityGroupID + RedisCacheAddress: !stack_output prd/redis-stack.yaml::RedisCacheAddress + RedisCachePort: !stack_output prd/redis-stack.yaml::RedisCachePort + RedisCacheUseSSL: "true" + VPCID: vpc-0e8902cf873551dcb # cdl-d2d-prd-vpc + SubnetIDs: + - subnet-09915dc8f8bd30423 # cdl-d2d-prd-public-2a + - subnet-06483f1e8e186fbca # cdl-d2d-prd-public-2b + RDSSecurityGroupID: sg-0e60820fecb39e7f4 # d2d-rds-sg (prd) + CertificateARN: arn:aws:acm:us-west-2:245555132524:certificate/3fd991e3-aa46-46c4-ba06-90225a9d667b + Route53ZoneID: Z032025711DX8JAAF4DUK + RDSCredentialsSecretARN: arn:aws:secretsmanager:us-west-2:245555132524:secret:prd/PIDService/MySQL-psNZHY + SMTPCredentialsSecretARN: arn:aws:secretsmanager:us-west-2:245555132524:secret:prd/SES/SMTP-oqv8wD + SessionSecretARN: arn:aws:secretsmanager:us-west-2:245555132524:secret:prd/PIDService/SessionSecret-foU4AW +sceptre_user_data: + load_balancer_security_group_sources: !stack_attr parameters.LoadBalancerSecurityGroupSources diff --git a/deployment/environment_config/dev_config.sh b/deployment/environment_config/dev_config.sh new file mode 100644 index 0000000..e8e2393 --- /dev/null +++ b/deployment/environment_config/dev_config.sh @@ -0,0 +1,4 @@ +AWS_PROFILE=cdl-d2d-dev +AWS_SERVICE_NAME=pidservice +AWS_REGION=us-west-2 +AWS_ACCT=445017934155 \ No newline at end of file diff --git a/deployment/environment_config/prd_config.sh b/deployment/environment_config/prd_config.sh new file mode 100644 index 0000000..6108130 --- /dev/null +++ b/deployment/environment_config/prd_config.sh @@ -0,0 +1,4 @@ +AWS_PROFILE=cdl-d2d-prd +AWS_SERVICE_NAME=pidservice +AWS_REGION=us-west-2 +AWS_ACCT=245555132524 \ No newline at end of file diff --git a/deployment/scripts/ecr_push.sh b/deployment/scripts/ecr_push.sh new file mode 100755 index 0000000..75720b6 --- /dev/null +++ b/deployment/scripts/ecr_push.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +if [[ "$ENVIRONMENT" == "dev" ]]; then + source ../environment_config/dev_config.sh +elif [[ "$ENVIRONMENT" == "prd" ]]; then + source ../environment_config/prd_config.sh +else + echo "FAILED: Unsupported environment." + exit 1 +fi + +# Check if logged in, and log in if not +if ! aws sts get-caller-identity --profile "$AWS_PROFILE" > /dev/null 2>&1; then + echo "===== Running AWS SSO =====" + if ! aws sso login --profile "$AWS_PROFILE"; then + echo "FAILED: AWS SSO login unsuccessful." + exit 1 + fi +else + echo "===== Already ran AWS SSO, proceeding =====" +fi + +set -euo pipefail + +DESTINATION_DIR=$(mktemp -d) +DOCKER_IMAGE_URI="$AWS_ACCT.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_SERVICE_NAME-$ENVIRONMENT:latest" + +cleanup() { + echo "===== Cleaning Up Temp Directories and Deleting Docker Images =====" + rm -rf "$DESTINATION_DIR" + docker rmi $DOCKER_IMAGE_URI +} + +trap "cleanup" EXIT + +echo "===== Logging in to AWS ECR =====" +aws ecr get-login-password --profile "$AWS_PROFILE" | docker login --username AWS --password-stdin $AWS_ACCT.dkr.ecr.$AWS_REGION.amazonaws.com + +echo "===== Cloning Code Repo =====" +REPO_URL="https://github.com/cdlib/pid.git" +git clone $REPO_URL $DESTINATION_DIR + +echo "===== Updating App Configs =====" +CONFIG_DIR=../app_config +cp $CONFIG_DIR/* $DESTINATION_DIR/app/config/ + +echo "===== Building Dockerfile =====" +export DOCKER_DEFAULT_PLATFORM=linux/amd64 +docker build --no-cache --tag $DOCKER_IMAGE_URI --file $DESTINATION_DIR/app/Dockerfile.app --target final $DESTINATION_DIR/app + +echo "===== Pushing image to ECR =====" +docker push $DOCKER_IMAGE_URI + +echo "SUCCESS: Pushed image to ECR." diff --git a/deployment/scripts/ecs_restart.sh b/deployment/scripts/ecs_restart.sh new file mode 100755 index 0000000..6a5b304 --- /dev/null +++ b/deployment/scripts/ecs_restart.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +if [[ "$ENVIRONMENT" == "dev" ]]; then + source ../environment_config/dev_config.sh +elif [[ "$ENVIRONMENT" == "prd" ]]; then + source ../environment_config/prd_config.sh +else + echo "FAILED: Unsupported environment." + exit 1 +fi + +# Check if logged in, and log in if not +if ! aws sts get-caller-identity --profile "$AWS_PROFILE" > /dev/null 2>&1; then + echo "===== Running AWS SSO =====" + if ! aws sso login --profile "$AWS_PROFILE"; then + echo "FAILED: AWS SSO login unsuccessful." + exit 1 + fi +else + echo "===== Already ran AWS SSO, proceeding =====" +fi + +ECS_CLUSTER="$AWS_SERVICE_NAME-$ENVIRONMENT-ECSCluster" +ECS_SERVICE="$AWS_SERVICE_NAME-$ENVIRONMENT-ECSService" + +echo "===== Displaying Variables =====" +echo "AWS Profile: $AWS_PROFILE" +echo "AWS Region: $AWS_REGION" +echo "ECS Cluster: $ECS_CLUSTER" +echo "ECS Service: $ECS_SERVICE" + +echo "===== Updating ECS service to restart tasks =====" +OUTPUT=$(aws ecs update-service \ + --profile "$AWS_PROFILE" \ + --region "$AWS_REGION" \ + --cluster "$ECS_CLUSTER" \ + --service "$ECS_SERVICE" \ + --force-new-deployment \ + --output yaml) + +if [ $? -ne 0 ]; then + echo "FAILED: ECS service update unsuccessful." +else + echo "SUCCESS:\n$OUTPUT" +fi \ No newline at end of file diff --git a/deployment/templates/redis-stack.yaml b/deployment/templates/redis-stack.yaml new file mode 100644 index 0000000..7b350c6 --- /dev/null +++ b/deployment/templates/redis-stack.yaml @@ -0,0 +1,54 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Redis Stack for PID Service + +Parameters: + ServiceName: + Type: String + Description: The name of the service + Environment: + Type: String + AllowedValues: + - dev + - prd + Description: The environment to deploy to + VPCID: + Type: AWS::EC2::VPC::Id + Description: The VPC ID + SubnetIDs: + Type: List + Description: The list of subnet IDs + +Resources: + RedisCacheSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for Redis cache + GroupName: !Sub "${ServiceName}-${Environment}-RedisCacheSecurityGroup" + VpcId: !Ref VPCID + + RedisCacheSecurityGroupIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !GetAtt RedisCacheSecurityGroup.GroupId + IpProtocol: tcp + FromPort: 6379 + ToPort: 6379 + SourceSecurityGroupId: !GetAtt RedisCacheSecurityGroup.GroupId + + RedisCache: + Type: AWS::ElastiCache::ServerlessCache + Properties: + Engine: redis + SecurityGroupIds: + - !GetAtt RedisCacheSecurityGroup.GroupId + ServerlessCacheName: !Sub "${ServiceName}-${Environment}-RedisCache" + SubnetIds: !Ref SubnetIDs + +Outputs: + RedisCacheSecurityGroupID: + Value: !GetAtt RedisCacheSecurityGroup.GroupId + RedisCacheAddress: + Value: !GetAtt RedisCache.Endpoint.Address + RedisCachePort: + Value: 6379 + # Value: !GetAtt RedisCache.Endpoint.Port \ No newline at end of file diff --git a/deployment/templates/service-stack.yaml.j2 b/deployment/templates/service-stack.yaml.j2 new file mode 100644 index 0000000..0362014 --- /dev/null +++ b/deployment/templates/service-stack.yaml.j2 @@ -0,0 +1,456 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Service Stack for PID Service + +Parameters: + ServiceName: + Type: String + Description: The name of the service + Environment: + Type: String + AllowedValues: + - dev + - prd + Description: The environment to deploy to + + MainSiteName: + Type: String + Description: The name of the main site + ContainerPort: + Type: Number + Description: The port of the container + LoadBalancerSecurityGroupSources: + Type: CommaDelimitedList + Description: > + This is unused, but is here for visibility in the CloudFormation console. + The value is the exact same as "sceptre_user_data.load_balancer_security_group_sources". + Ideally CloudFormation would support looping so this can be used directly. + + DeadPIDURL: + Type: String + Description: The URL to use for the dead PID check + AccountLockEmailTo: + Type: String + Description: The email address to send account lock notifications to + SMTPSenderAddress: + Type: String + Description: The email address to use as the sender for emails + + RedisCacheSecurityGroupID: + Type: String + Description: The security group ID for the Redis cache + RedisCacheAddress: + Type: String + Description: The host to use for redis + RedisCachePort: + Type: Number + Description: The port to use for redis + RedisCacheUseSSL: + Type: String + Default: "true" + Description: Whether or not to use SSL for redis + + VPCID: + Type: AWS::EC2::VPC::Id + Description: The VPC ID + SubnetIDs: + Type: List + Description: The list of subnet IDs + RDSSecurityGroupID: + Type: AWS::EC2::SecurityGroup::Id + Description: The security group ID for the RDS database + + CertificateARN: + Type: String + Description: The ARN of the certificate to use for HTTPS + Route53ZoneID: + Type: String + Description: The ID of the Route53 zone + + RDSCredentialsSecretARN: + Type: String + Description: The ARN of the Secrets Manager secret containing database credentials + + SMTPCredentialsSecretARN: + Type: String + Description: The ARN of the Secrets Manager secret containing SMTP credentials + + SessionSecretARN: + Type: String + Description: The ARN of the Secrets Manager secret containing the session secret + +Conditions: + InDevEnvironment: + !Equals [!Ref Environment, dev] + +Resources: + ######## + # Logs # + ######## + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "${ServiceName}-${Environment}-LogGroup" + RetentionInDays: 14 + UpdateReplacePolicy: Retain + # DeletionPolicy: Retain + + ############ + # Security # + ############ + LoadBalancerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for the Load Balancer + GroupName: !Sub "${ServiceName}-${Environment}-LoadBalancerSecurityGroup" + VpcId: !Ref VPCID + SecurityGroupIngress: + {%- for source in sceptre_user_data.load_balancer_security_group_sources %} + {%- for port in [80, 443] %} + {%- if source[:2] == "pl" %} + - SourcePrefixListId: {{ source }} + {%- else %} + - CidrIp: {{ source }} + {%- endif %} + IpProtocol: tcp + FromPort: {{ port }} + ToPort: {{ port }} + {%- endfor %} + {%- endfor %} + + ECSServiceSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for ECS tasks + GroupName: !Sub "${ServiceName}-${Environment}-ECSServiceSecurityGroup" + SecurityGroupIngress: + - SourceSecurityGroupId: !GetAtt LoadBalancerSecurityGroup.GroupId + IpProtocol: tcp + FromPort: !Ref ContainerPort + ToPort: !Ref ContainerPort + VpcId: !Ref VPCID + + ########### + # Network # + ########### + LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + LoadBalancerAttributes: + # this is the default, but is specified here in case it needs to be changed + - Key: idle_timeout.timeout_seconds + Value: 1200 # 20 Minutes checked with SCP + Name: !Sub "${ServiceName}-${Environment}-LoadBalancer" + Scheme: internet-facing + SecurityGroups: [!GetAtt LoadBalancerSecurityGroup.GroupId] + Subnets: !Ref SubnetIDs + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 30 + # will look for a 200 status code by default unless specified otherwise + HealthCheckPath: "/" + HealthCheckTimeoutSeconds: 5 + UnhealthyThresholdCount: 2 + HealthyThresholdCount: 2 + Name: !Sub "${ServiceName}-${Environment}-TargetGroup" + Port: !Ref ContainerPort + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # default is 300 + TargetType: ip + VpcId: !Ref VPCID + + LoadBalancerListenerHTTP: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + # - TargetGroupArn: !GetAtt TargetGroup.TargetGroupArn + # Type: forward + - Type: redirect + RedirectConfig: + Host: "#{host}" + Path: "/#{path}" + Port: 443 + Protocol: HTTPS + Query: "#{query}" + StatusCode: HTTP_301 + LoadBalancerArn: !Ref LoadBalancer + Port: !Ref ContainerPort + Protocol: HTTP + + LoadBalancerListenerHTTPS: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + - TargetGroupArn: !GetAtt TargetGroup.TargetGroupArn + Type: forward + LoadBalancerArn: !Ref LoadBalancer + Port: 443 + Protocol: HTTPS + Certificates: + - CertificateArn: !Ref CertificateARN + SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 + + MainSiteDNS: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref Route53ZoneID + Type: A + Name: !Ref MainSiteName + AliasTarget: + DNSName: !GetAtt LoadBalancer.DNSName + HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID + + ######### + # Roles # + ######### + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${ServiceName}-${Environment}-FargateTaskExecutionRole" + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Policies: + - PolicyName: allow-fargate-to-set-up-func + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ServiceName}-${Environment}" + - Effect: Allow + Action: ecr:GetAuthorizationToken + Resource: '*' + - PolicyName: allow-func-to-log + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt LogGroup.Arn + - PolicyName: allow-func-to-access-secrets + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref RDSCredentialsSecretARN + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref SMTPCredentialsSecretARN + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Ref SessionSecretARN + + ScheduleRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${ServiceName}-${Environment}-ScheduleRole" + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: scheduler.amazonaws.com + Policies: + - PolicyName: allow-events-to-run-task + PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: ecs:RunTask + Condition: + ArnEquals: + ecs:cluster: !GetAtt ECSCluster.Arn + Effect: Allow + Resource: !Ref TaskDefinition + - Action: iam:PassRole + Effect: Allow + Resource: !GetAtt ECSTaskExecutionRole.Arn + + ############ + # Services # + ############ + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub "${ServiceName}-${Environment}-ECSCluster" + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + ContainerDefinitions: + - Essential: true + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ServiceName}-${Environment}:latest" + Name: !Sub "${ServiceName}-${Environment}-FargateContainer" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref LogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: !Sub "${ServiceName}-${Environment}-FargateContainer" + PortMappings: + - AppProtocol: http + ContainerPort: !Ref ContainerPort + HostPort: !Ref ContainerPort + Protocol: tcp + Environment: + - Name: RACK_ENV + Value: !If [InDevEnvironment, development, production] + - Name: REDIS_HOST + Value: !Ref RedisCacheAddress + - Name: REDIS_PORT + Value: !Ref RedisCachePort + - Name: REDIS_USE_SSL + Value: !Ref RedisCacheUseSSL + - Name: APP_HOST + Value: !Ref MainSiteName + - Name: APP_PORT + Value: !Ref ContainerPort + - Name: DEAD_PID_URL + Value: !Ref DeadPIDURL + - Name: ACCOUNT_LOCK_EMAIL_TO + Value: !Ref AccountLockEmailTo + - Name: SMTP_SENDER_ADDRESS + Value: !Ref SMTPSenderAddress + Secrets: + - Name: DB_USERNAME + ValueFrom: !Sub "${RDSCredentialsSecretARN}:DB_USERNAME::" + - Name: DB_PASSWORD + ValueFrom: !Sub "${RDSCredentialsSecretARN}:DB_PASSWORD::" + - Name: DB_NAME + ValueFrom: !Sub "${RDSCredentialsSecretARN}:DB_NAME::" + - Name: DB_HOST + ValueFrom: !Sub "${RDSCredentialsSecretARN}:DB_HOST::" + - Name: SMTP_HOST + ValueFrom: !Sub "${SMTPCredentialsSecretARN}:SMTP_HOST::" + - Name: SMTP_PORT + ValueFrom: !Sub "${SMTPCredentialsSecretARN}:SMTP_PORT::" + - Name: SMTP_USERNAME + ValueFrom: !Sub "${SMTPCredentialsSecretARN}:SMTP_USERNAME::" + - Name: SMTP_PASSWORD + ValueFrom: !Sub "${SMTPCredentialsSecretARN}:SMTP_PASSWORD::" + - Name: SESSION_SECRET + ValueFrom: !Sub "${SessionSecretARN}:SESSION_SECRET::" + # HealthCheck: + # Command: + # - CMD-SHELL + # - curl -s -f localhost:${ContainerPort}/public/search | grep -q 'PID Service' || exit 1 + # Interval: 30 + # Retries: 3 + # StartPeriod: 60 + # Timeout: 45 + Cpu: "256" + ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn + Family: !Sub "${ServiceName}-${Environment}-FargateTaskDefinition" + Memory: "512" + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + + ECSService: + Type: AWS::ECS::Service + DependsOn: + - LoadBalancerListenerHTTP + - LoadBalancerListenerHTTPS + Properties: + Cluster: !Ref ECSCluster + LaunchType: FARGATE + DeploymentConfiguration: + DeploymentCircuitBreaker: + Enable: true + Rollback: true + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !GetAtt ECSServiceSecurityGroup.GroupId + - !Ref RDSSecurityGroupID + - !Ref RedisCacheSecurityGroupID + Subnets: !Ref SubnetIDs + ServiceName: !Sub "${ServiceName}-${Environment}-ECSService" + TaskDefinition: !Ref TaskDefinition + LoadBalancers: + - ContainerName: !Sub "${ServiceName}-${Environment}-FargateContainer" + ContainerPort: !Ref ContainerPort + TargetGroupArn: !GetAtt TargetGroup.TargetGroupArn + Tags: + - Key: Environment + Value: !Ref Environment + - Key: Program + Value: d2d + - Key: Service + Value: !Ref ServiceName + + ############# + # Schedules # + ############# + ScheduleGroup: + Type: AWS::Scheduler::ScheduleGroup + Properties: + Name: !Sub "${ServiceName}-${Environment}-ScheduleGroup" + + RedisSyncSchedule: + Type: AWS::Scheduler::Schedule + Properties: + FlexibleTimeWindow: + Mode: "OFF" # AWS Documentation says to put this in quotes. + GroupName: !Ref ScheduleGroup + Name: !Sub "${ServiceName}-${Environment}-RedisSyncSchedule" + ScheduleExpression: cron(30 9 ? * SAT *) + State: ENABLED + Target: + Arn: !GetAtt ECSCluster.Arn + EcsParameters: + LaunchType: FARGATE + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !GetAtt ECSServiceSecurityGroup.GroupId + - !Ref RDSSecurityGroupID + - !Ref RedisCacheSecurityGroupID + Subnets: !Ref SubnetIDs + TaskCount: 1 + TaskDefinitionArn: !Ref TaskDefinition + Input: !Sub '{"containerOverrides":[{"name":"${ServiceName}-${Environment}-FargateContainer","command":["ruby","ruby_scripts/synchronize_redis.rb"]}]}' + RoleArn: !GetAtt ScheduleRole.Arn + + DuplicateURLDetectSchedule: + Type: AWS::Scheduler::Schedule + Properties: + FlexibleTimeWindow: + Mode: "OFF" # AWS Documentation says to put this in quotes. + GroupName: !Ref ScheduleGroup + Name: !Sub "${ServiceName}-${Environment}-DuplicateURLDetectSchedule" + ScheduleExpression: cron(0 9 ? * SAT *) + State: ENABLED + Target: + Arn: !GetAtt ECSCluster.Arn + EcsParameters: + LaunchType: FARGATE + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - !GetAtt ECSServiceSecurityGroup.GroupId + - !Ref RDSSecurityGroupID + - !Ref RedisCacheSecurityGroupID + Subnets: !Ref SubnetIDs + TaskCount: 1 + TaskDefinitionArn: !Ref TaskDefinition + Input: !Sub '{"containerOverrides":[{"name":"${ServiceName}-${Environment}-FargateContainer","command":["ruby","ruby_scripts/detect_duplicate_urls.rb"]}]}' + RoleArn: !GetAtt ScheduleRole.Arn diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..400f667 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "pid" +version = "0.1.0" +description = "PID Service" +authors = [] +readme = "README.md" +requires-python = ">=3.13,<3.14" +dependencies = [] + +[dependency-groups] +dev = [ + "sceptre>=4.5.3", + "sceptre-ssm-resolver>=1.2.2", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9a0ca5f --- /dev/null +++ b/uv.lock @@ -0,0 +1,408 @@ +version = 1 +revision = 3 +requires-python = "==3.13.*" + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/37/78c630d1308964aa9abf44951d9c4df776546ff37251ec2434944e205c4e/boto3-1.43.6.tar.gz", hash = "sha256:e6315effaf12b890b99956e6f8e2c3000a3f64e4ee91943cec3895ce9a836afb", size = 113153, upload-time = "2026-05-07T20:49:59.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/e2/3c2eef44f55eafab256836d1d9479bd6a74f70c26cbfdc0639a0e23e4327/boto3-1.43.6-py3-none-any.whl", hash = "sha256:179601ec2992726a718053bf41e43c223ceba397d31ceab11f64d9c910d9fc3a", size = 140502, upload-time = "2026-05-07T20:49:57.8Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/a7/23d0f5028011455096a1eeac0ddf3cbe147b3e855e127342f8202552194d/botocore-1.43.6.tar.gz", hash = "sha256:b1e395b347356860398da42e61c808cf1e34b6fa7180cf2b9d87d986e1a06ba0", size = 15336070, upload-time = "2026-05-07T20:49:48.14Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/c8/6f47223840e8d8cfa8c9f7c0ec1b77970417f257fc885169ff4f6326ce09/botocore-1.43.6-py3-none-any.whl", hash = "sha256:b6d1fdbc6f65a5fe0b7e947823aa37535d3f39f3ba4d21110fab1f55bbbcc04b", size = 15017094, upload-time = "2026-05-07T20:49:44.964Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cfn-flip" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/75/8eba0bb52a6c58e347bc4c839b249d9f42380de93ed12a14eba4355387b4/cfn_flip-1.3.0.tar.gz", hash = "sha256:003e02a089c35e1230ffd0e1bcfbbc4b12cc7d2deb2fcc6c4228ac9819307362", size = 16113, upload-time = "2021-10-07T10:05:14.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/a8/a67297cd63ef99c3391c1d143161b187b71afa715a988655758269e3d02f/cfn_flip-1.3.0-py3-none-any.whl", hash = "sha256:faca8e77f0d32fb84cce1db1ef4c18b14a325d31125dae73c13bcc01947d2722", size = 21387, upload-time = "2021-10-07T10:05:13.378Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/75/f2a4c0c94c85e2693c229142eb448840fba0f9230111faa889d1f541d12d/colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1", size = 27328, upload-time = "2019-12-06T20:46:32.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/dc/45cdef1b4d119eb96316b3117e6d5708a08029992b2fee2c143c7a0a5cc5/colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", size = 15931, upload-time = "2019-12-06T20:46:31.424Z" }, +] + +[[package]] +name = "deepdiff" +version = "8.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderly-set" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/50/767448e792d41bfb6094ee317a355c1cb221dca24b2e178e2203bbea2a77/deepdiff-8.6.2.tar.gz", hash = "sha256:186dcbd181e4d76cef11ab05f802d0056c5d6083c5a6748c1473e9d7481e183e", size = 634860, upload-time = "2026-03-18T17:16:33.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/5f/c52bd1255db763d0cdcb7084d2e90c42119cb229302c56bdf1d0aa78abd2/deepdiff-8.6.2-py3-none-any.whl", hash = "sha256:4d22034a866c3928303a9332c279362f714192d9305bac17c498720d095fd1b4", size = 91979, upload-time = "2026-03-18T17:16:32.171Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "idna" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonschema" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pyrsistent" }, + { name = "setuptools" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/11/a69e2a3c01b324a77d3a7c0570faa372e8448b666300c4117a516f8b1212/jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a", size = 167226, upload-time = "2019-11-18T12:57:10.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/8f/51e89ce52a085483359217bc72cdbf6e75ee595d5b1d4b5ade40c7e018b8/jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", size = 56305, upload-time = "2019-11-18T12:57:08.454Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, +] + +[[package]] +name = "networkx" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/97/ae/7497bc5e1c84af95e585e3f98585c9f06c627fac6340984c4243053e8f44/networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51", size = 1844862, upload-time = "2021-09-09T22:09:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/93/aa6613aa70d6eb4868e667068b5a11feca9645498fd31b954b6c4bb82fa5/networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef", size = 1927288, upload-time = "2021-09-09T22:09:39.016Z" }, +] + +[[package]] +name = "orderly-set" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "pid" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "sceptre" }, + { name = "sceptre-ssm-resolver" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "sceptre", specifier = ">=4.5.3" }, + { name = "sceptre-ssm-resolver", specifier = ">=1.2.2" }, +] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642, upload-time = "2023-10-25T21:06:56.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106, upload-time = "2023-10-25T21:06:54.387Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "requests" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, +] + +[[package]] +name = "sceptre" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "cfn-flip" }, + { name = "click" }, + { name = "colorama" }, + { name = "deepdiff" }, + { name = "deprecation" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "networkx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sceptre-cmd-resolver" }, + { name = "sceptre-file-resolver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/d2/5f4a29e81dd4364d5b2086d84eb37b4978f8a10b134efbffcf8f92a599e4/sceptre-4.6.0.tar.gz", hash = "sha256:a60cb79d3c6180cc055aa1e44f2c4c020018cae23ee428ee66ff101915f74a81", size = 86499, upload-time = "2026-01-27T15:59:00.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/42/8d3852fb0dcb2b3478bc29d26ba37228c7ca9717e2dc51a07ad1820ed55e/sceptre-4.6.0-py3-none-any.whl", hash = "sha256:9d2eb9388d94b0d131092173664a7516d5c953728c06d60144169f8827c0b87e", size = 111021, upload-time = "2026-01-27T15:59:01.547Z" }, +] + +[[package]] +name = "sceptre-cmd-resolver" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sceptre" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/80/acb986323af0b3e5e3eb59bb293e6671cdc43ded91620a24a1a58b2e28f7/sceptre-cmd-resolver-2.0.0.tar.gz", hash = "sha256:155c47e2f4f55c7b6eb64bfe8760174701442ecaddba1a6f5cb7715a1c95be99", size = 4307, upload-time = "2023-02-13T15:23:27.691Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/8f/465ac4b87e967d9d023aaad7a9595d82e1cfdb5d532c8f50ab046adef0e3/sceptre_cmd_resolver-2.0.0-py2.py3-none-any.whl", hash = "sha256:eea8ce4cfcd9199f726b4280e7e35923c9d4ea5d75cbe4a8ee78c0d6d2996d09", size = 4659, upload-time = "2023-02-13T15:23:26.603Z" }, +] + +[[package]] +name = "sceptre-file-resolver" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "sceptre" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/20/c8162b958668c741bef1d7d247a78f796b705ed0eec72501ef308110923b/sceptre-file-resolver-1.0.6.tar.gz", hash = "sha256:d47cfe32d141fb46467fcd319bf4386f0178cf0c2211c6f1d2dffbc80d785a6d", size = 3748, upload-time = "2022-03-07T17:32:43.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/70/174f2395655d7736219645182f894bba9c43c7cc97cec540ac3e230b46b1/sceptre_file_resolver-1.0.6-py2.py3-none-any.whl", hash = "sha256:bba0465a90681ea1d45260c86c3feaf583469f7ddc07e0cd97edcbbb96b459ce", size = 7485, upload-time = "2022-03-07T17:32:42.53Z" }, +] + +[[package]] +name = "sceptre-ssm-resolver" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sceptre" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/6a/97b050b5aa9df6ba3cc3efc5b6b18087365d59e9c4265d6bd3a4b105e56d/sceptre-ssm-resolver-1.2.2.tar.gz", hash = "sha256:dbff1b33b2f86e4e2349f3e3a583f46065596ac57f3623d5fc3243e150a97f5c", size = 4002, upload-time = "2022-03-07T15:12:11.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8e/f307d0ccb362d3daca3ea1090e6c30177bd68154f375950779d744df1bba/sceptre_ssm_resolver-1.2.2-py2.py3-none-any.whl", hash = "sha256:caf2713190e96af20569c48c1b5c4f42ea29838e011493432cc7a69f21b9fe02", size = 3845, upload-time = "2022-03-07T15:12:09.672Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From ada760ce47991fe63da28dd46a58fa50bf8ad830 Mon Sep 17 00:00:00 2001 From: Christopher Groskopf Date: Tue, 12 May 2026 13:56:14 -0700 Subject: [PATCH 2/2] Add MySQL to Docker Compose. Simplify build process. --- .env.example | 13 ++--- .gitignore | 1 + README.md | 6 +-- app/Dockerfile.app | 5 +- app/README.md | 5 +- deployment/scripts/ecr_push.sh | 1 - docker-compose.yml | 98 ++++++++++++++++++++++++++++------ redis/Dockerfile | 3 +- 8 files changed, 98 insertions(+), 34 deletions(-) diff --git a/.env.example b/.env.example index 986efe5..2160e2f 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,8 @@ APP_HOST='localhost' APP_PORT='80' # If you change the port, you must reflect it in the docker-compose file! DEAD_PID_URL='http://localhost:80/link/inactive' # If you change the port you must update this URL. -SESSION_SECRET='your_session_secret_here' +# Must be at least 64 characters. Generate a real local value with: openssl rand -hex 32 +SESSION_SECRET='0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' ACCOUNT_LOCK_EMAIL_TO='your_email_here' # Redis @@ -14,14 +15,14 @@ REDIS_PORT='6379' # Default port for redis. If you change the port, you must ref REDIS_USE_SSL='false' # Database Credentials (Secrets) -DB_HOST='host.docker.internal' # References the host machine for the docker container -DB_NAME='your_db_name_here' -DB_USERNAME='your_db_username_here' -DB_PASSWORD='your_db_password_here' +DB_HOST='mysql' # References the mysql container in docker-compose.yml +DB_NAME='pid_dev' +DB_USERNAME='pid' +DB_PASSWORD='devpassword' # SMTP Credentials (Secrets) SMTP_HOST='your_smtp_host_here' SMTP_PORT='587' # Default port for SMTP SMTP_USERNAME='your_smtp_username_here' SMTP_PASSWORD='your_smtp_password_here' -SMTP_SENDER_ADDRESS='your_email_here' \ No newline at end of file +SMTP_SENDER_ADDRESS='your_email_here' diff --git a/.gitignore b/.gitignore index 0007158..d0250d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.rdb *.log .DS_Store +.data/ vendor app/config/*.yml log/*_log diff --git a/README.md b/README.md index ac48a05..f90bd93 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,15 @@ When the third party moves that article to some.site.org/new/path/to/same/file.h The whole application, including dependencies like Redis, is Dockerized and can be run as a container. Libraries and packages are managed by Bundler; please refer to the Gemfile and Gemfile.lock files for more information. Aside from those, here is a list of things you need to run the application on your machine: - [Docker](https://www.docker.com/products/docker-desktop/) -- A MySQL database to act as a database for the service (see the Database Structure section). +- A MySQL database to act as a database for the service (see the Database Structure section). The Docker Compose setup includes a local MySQL container for development. - SMTP server for sending emails. This only concerns the password reset feature. ## Installation To run the application locally, do the following: - Install Docker -- Make sure you have a MySQL database ready for this system to use. +- Make sure you have a MySQL database ready for this system to use, or use the MySQL container included in the Docker Compose setup. - Optionally get an SMTP server for sending emails. - Clone the repository: `git clone https://github.com/cdlib/pid` - Refer to the [README](https://github.com/cdlib/pid/blob/master/app/README.md) file in the app directory for more information on running the application. -To deploy the application in the cloud, refer to the [pid-env](https://github.com/cdlib/pid-env/tree/master) repository. \ No newline at end of file +To deploy the application in the cloud, refer to the [pid-env](https://github.com/cdlib/pid-env/tree/master) repository. diff --git a/app/Dockerfile.app b/app/Dockerfile.app index ed113ba..6e0670b 100644 --- a/app/Dockerfile.app +++ b/app/Dockerfile.app @@ -1,7 +1,6 @@ -ARG TARGET_PLATFORM=linux/amd64 ARG IMAGE_TAG=3.3.7-slim-bullseye -FROM --platform=${TARGET_PLATFORM} ruby:${IMAGE_TAG} AS base +FROM ruby:${IMAGE_TAG} AS base RUN apt-get update -qq \ && apt-get install -y \ @@ -26,7 +25,7 @@ COPY Gemfile Gemfile.lock ./ RUN bundle config set --local without 'test' && \ bundle install -FROM --platform=$TARGET_PLATFORM base AS final +FROM base AS final COPY --from=dependencies /usr/local/bundle /usr/local/bundle diff --git a/app/README.md b/app/README.md index 148af20..5b99ee7 100644 --- a/app/README.md +++ b/app/README.md @@ -8,12 +8,13 @@ > Generate a 64-character hex for the session secret. The SMTP stuff is optional. - Replace `/app/config/*.yml.example` files with `*.yml` versions. > The `.example` files are primarily for reference, though they will be sufficient to get the app running; you can modify the values to implement your own configuration. Naturally, if you wish to connect to an external database, you may need to be on VPN. + > The default `.env.example` database values use the local MySQL container defined in `docker-compose.yml`. ## Docker Installation ### Running the Appplication - Using `docker-compose`: - Run `docker-compose up --build` to build and start the application. - > This will build the Redis container as well as the application container, but only after making sure the tests pass. You can modify the `docker-compose.yml` file to skip the tests. + > This will build MySQL, Redis, the schema bootstrap container, and the application container, but only after making sure the tests pass. You can modify the `docker-compose.yml` file to skip the tests. - As you update the application, rebuild by running `docker-compose build`. - To start the application without or after rebuilding, run `docker-compose up`. - To tear down, run `docker-compose down`. @@ -151,4 +152,4 @@ MySQL Table Overview - **Invalid_url_reports** - A table that stores a list of invalid URLs (i.e. Pinging the URL returns an HTTP >= 400 status code). This table is populated by a job that is kicked off by Cron each weekend. - **Skip_checks** - A table that stores the domains of URLs were a contractually not allowed to run the invalid URLs check against -Please refer to the schema file `/app/db/schema.rb` to see the structure of the database. The schema file was generated by Active Record while connected to the actual database for the service. It is currently used to initialize the database for testing with SQLite, so it should be a good reference, though unfortunately it doesn't include the indexes and constraints. Please contact one of the developers for more information. \ No newline at end of file +Please refer to the schema file `/app/db/schema.rb` to see the structure of the database. The schema file was generated by Active Record while connected to the actual database for the service. It is currently used to initialize the database for testing with SQLite, so it should be a good reference, though unfortunately it doesn't include the indexes and constraints. Please contact one of the developers for more information. diff --git a/deployment/scripts/ecr_push.sh b/deployment/scripts/ecr_push.sh index 75720b6..9f493e5 100755 --- a/deployment/scripts/ecr_push.sh +++ b/deployment/scripts/ecr_push.sh @@ -45,7 +45,6 @@ CONFIG_DIR=../app_config cp $CONFIG_DIR/* $DESTINATION_DIR/app/config/ echo "===== Building Dockerfile =====" -export DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build --no-cache --tag $DOCKER_IMAGE_URI --file $DESTINATION_DIR/app/Dockerfile.app --target final $DESTINATION_DIR/app echo "===== Pushing image to ECR =====" diff --git a/docker-compose.yml b/docker-compose.yml index 17790e2..2f68c9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,98 @@ services: + mysql: + image: mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: devpassword + MYSQL_DATABASE: pid_dev + MYSQL_USER: pid + MYSQL_PASSWORD: devpassword + ports: + - "3306:3306" + volumes: + - .data/mysql:/var/lib/mysql + healthcheck: + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "root", + "-pdevpassword", + ] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + bootstrap: + build: + context: ./app + dockerfile: Dockerfile.app + target: final + env_file: + - .env + environment: + DB_HOST: mysql + DB_NAME: pid_dev + DB_USERNAME: pid + DB_PASSWORD: devpassword + command: + - ruby + - -r + - active_record + - -e + - | + ActiveRecord::Base.establish_connection( + adapter: 'mysql2', + encoding: 'utf8', + host: ENV.fetch('DB_HOST'), + port: 3306, + database: ENV.fetch('DB_NAME'), + username: ENV.fetch('DB_USERNAME'), + password: ENV.fetch('DB_PASSWORD') + ) + load 'db/schema.rb' + depends_on: + mysql: + condition: service_healthy + test: build: - context: ./app # Build context is the app directory - dockerfile: Dockerfile.test # Use Dockerfile.test - args: - - TARGET_PLATFORM=linux/arm64 # Change depending on the target platform + context: ./app # Build context is the app directory + dockerfile: Dockerfile.test # Use Dockerfile.test depends_on: - - redis # Ensure the Redis container is started first + redis: + condition: service_started env_file: - - .env # Load environment variables from the .env file + - .env # Load environment variables from the .env file app: build: - context: ./app # Build context is the app directory - dockerfile: Dockerfile.app # Use Dockerfile.app + context: ./app # Build context is the app directory + dockerfile: Dockerfile.app # Use Dockerfile.app target: final - args: - - TARGET_PLATFORM=linux/arm64 # Change depending on the target platform ports: - "80:80" depends_on: - - test # Ensure that the tests pass first - - redis # Ensure the Redis container is started first + bootstrap: + condition: service_completed_successfully + test: + condition: service_completed_successfully + redis: + condition: service_started env_file: - - .env # Load environment variables from the .env file + - .env # Load environment variables from the .env file + environment: + DB_HOST: mysql + DB_NAME: pid_dev + DB_USERNAME: pid + DB_PASSWORD: devpassword redis: build: - context: ./redis # Build context is the redis directory - args: - - TARGET_PLATFORM=linux/arm64 # Change depending on the target platform + context: ./redis # Build context is the redis directory ports: - - "6379:6379" # Map host port to container port + - "6379:6379" # Map host port to container port diff --git a/redis/Dockerfile b/redis/Dockerfile index 1356ed1..b149670 100644 --- a/redis/Dockerfile +++ b/redis/Dockerfile @@ -1,7 +1,6 @@ -ARG TARGET_PLATFORM=linux/amd64 ARG IMAGE_TAG=latest -FROM --platform=${TARGET_PLATFORM} redis:${IMAGE_TAG} +FROM redis:${IMAGE_TAG} COPY redis.conf /usr/local/etc/redis/redis.conf