uc3-dmp-api-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7033c9283948ba0b3b9a2cea45d17ba843f42b2d78f8a32f003d88cce9532c9e
4
+ data.tar.gz: 7fbfe97abe42ee3005c37b246eac7eece4358cceb42a9c7d8fe4e1ae13f5c8ac
5
+ SHA512:
6
+ metadata.gz: d253494718f77f3cbf5e72d65f331eebe00a5fc327551c6478c1c144c29028b6bbab79395a76a15883049ff89482011015bb8387116407cb1d27edc64c8e24ae
7
+ data.tar.gz: fc0776170a8a1237f4b66564b83b12ff991548d6f98f4e8f43cce35414c3ba6a70e5e86f62155f710d09d230626510c46dcd64ad144d740dd7f5814e73abcf2b
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # Uc3DmpApiCore
2
+
3
+ Basic helper classes used by the DMPTool Lambda functions.
4
+
5
+ - Logger: Helper for logging messages and errors to CloudWatch
6
+ - Notifier: Helper for sending emails via SNS and sending events to EventBridge
7
+ - Responder: Helper that formats API responses in a standardized way
8
+ - SsmReader: Helper that fetches values from the SSM parameter store
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './notifier'
4
+
5
+ module Uc3DmpApiCore
6
+ # Standardized ways for logging messages and errors to CloudWatch
7
+ #
8
+ # Methods expect the following inputs:
9
+ # - source: the name of the Lambda function or Layer file
10
+ # - message: the error message as a String or Array of Strings
11
+ # - details: any additional context as a Hash
12
+ # - event: the Lambda event if available
13
+ #
14
+ # --------------------------------------------------------------------------------
15
+ class Logger
16
+ class << self
17
+ # rubocop:disable Metrics/AbcSize
18
+ def log_error(source:, message:, details: {}, event: {})
19
+ return false if source.nil? || message.nil?
20
+
21
+ message = message.join(', ') if message.is_a?(Array)
22
+ # Is there a better way here than just 'print'? This ends up in the CloudWatch logs
23
+ puts "ERROR: #{source} - #{message}"
24
+ puts " - DETAILS: #{details.to_json}" if details.is_a?(Hash) && details.keys.any?
25
+ puts " - EVENT: #{event.to_json}" if event.is_a?(Hash) && event.keys.any?
26
+
27
+ Notifier.notify_administrator(source: source, details: details, event: event)
28
+ end
29
+ # rubocop:enable Metrics/AbcSize
30
+
31
+ def log_message(source:, message:, details: {}, event: {})
32
+ return false if source.nil? || message.nil?
33
+
34
+ message = message.join(', ') if message.is_a?(Array)
35
+
36
+ # Is there a better way here than just 'print'? This ends up in the CloudWatch logs
37
+ puts "INFO: #{source} - #{message}"
38
+ puts " - DETAILS: #{details.to_json}" if details.is_a?(Hash) && details.keys.any?
39
+ true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+
5
+ module Uc3DmpApiCore
6
+ # Helper functions to send emails via SNS or publish events to EventBridge
7
+ class Notifier
8
+ class << self
9
+ # Sends the Administrator an email notification
10
+ # --------------------------------------------------------------------------------
11
+ # rubocop:disable Metrics/AbcSize
12
+ def notify_administrator(source:, details:, event: {})
13
+ Aws::SNS::Client.new.publish(
14
+ topic_arn: ENV.fetch('SNS_FATAL_ERROR_TOPIC', nil),
15
+ subject: "DMPTool - fatal error in - #{source}",
16
+ message: _build_admin_message(source: source, details: details, event: event)
17
+ )
18
+ true
19
+ rescue Aws::Errors::ServiceError => e
20
+ puts "Uc3DmpCore.Notifier - Unable to notify administrator via SNS! - #{e.message} - on #{source}"
21
+ puts " - EVENT: #{event.to_json}" if event.is_a?(Hash) && event.keys.any?
22
+ puts " - DETAILS: #{details.to_json}" if details.is_a?(Hash) && details.keys.any?
23
+ false
24
+ end
25
+ # rubocop:enable Metrics/AbcSize
26
+
27
+ private
28
+
29
+ # Format the Admin email message
30
+ def _build_admin_message(source:, details:, event: {})
31
+ payload = "DMPTool #{ENV.fetch('LAMBDA_ENV', 'dev')} has encountered a fatal error within a Lambda function.\n\n /
32
+ SOURCE: #{source}\n /
33
+ TIME STAMP: #{Time.now.strftime('%Y-%m-%dT%H:%M:%S%L%Z')}\n /
34
+ NOTES: Check the CloudWatch logs for additional details.\n\n"
35
+ payload += "CALLER RECEIVED: #{event.to_json}\n\n" if event.is_a?(Hash) && event.keys.any?
36
+ payload += "DETAILS: #{details.to_json}\n\n" if details.is_a?(Hash) && details.keys.any?
37
+ payload += "This is an automated email generated by the Uc3DmpCore.Notifier gem.\n /
38
+ Please do not reply to this message."
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uc3DmpApiCore
4
+ # Use Rails' ActiveResource to communicate with the DMPHub REST API
5
+ class Paginator
6
+ DEFAULT_PAGE = 1
7
+ DEFAULT_PER_PAGE = 25
8
+ MAXIMUM_PER_PAGE = 250
9
+
10
+ class << self
11
+ def paginate(params: {}, results:)
12
+ return results unless results.is_a?(Array) && results.any? && params.is_a?(Hash)
13
+
14
+ current = _current_page(item_count: item_count, params: params)
15
+ # Just return as is if there is only one page
16
+ return results if current[:total_pages] == 1 || current[:per_page] >= results.length
17
+
18
+ # Calculate the offset and extract those results
19
+ offset = current[:page] == 1 ? 0 : (current[:page] - 1) * current[:per_page]
20
+ results[offset,(current[:per_page] - 1)]
21
+ end
22
+
23
+ # Construct the pagination meta information that will be included in the response
24
+ def pagination_meta(url:, item_count: 0, params: {})
25
+ current = _current_page(item_count: item_count, params: params)
26
+ {
27
+ page: current[:page],
28
+ per_page: current[:per_page],
29
+ total_items: item_count,
30
+ first: _pagination_link(url: url, target_page: 1, per_page: per_page),
31
+ prev: _pagination_link(url: url, target_page: page - 1, per_page: per_page),
32
+ next: _pagination_link(url: url, target_page: page + 1, per_page: per_page),
33
+ last: _pagination_link(url: url, target_page: total_pages, per_page: per_page)
34
+ }.compact
35
+ end
36
+
37
+ private
38
+
39
+ # Fetch the current :page and :per_page from the params or use the defaults
40
+ def _current_page(item_count: 0, params: {})
41
+ page = params.fetch('page', DEFAULT_PAGE)
42
+ page = DEFAULT_PAGE if page <= 1
43
+ per_page = params.fetch('per_page', DEFAULT_PER_PAGE)
44
+ per_page = DEFAULT_PER_PAGE if per_page >= MAXIMUM_PER_PAGE || per_page <= 1
45
+
46
+ total_pages = _page_count(total: item_count, per_page: per_page)
47
+ page = total_pages if page > total_pages
48
+
49
+ { page: page.to_i, per_page: per_page.to_i, total_pages: total_pages.to_i }
50
+ end
51
+
52
+ # Generate a pagination link
53
+ # --------------------------------------------------------------------------------
54
+ def _pagination_link(url:, target_page:, per_page: DEFAULT_PER_PAGE)
55
+ return nil if url.nil? || target_page.nil?
56
+
57
+ link = _url_without_pagination(url: url)
58
+ return nil if link.nil?
59
+
60
+ link += '?' unless link.include?('?')
61
+ link += '&' unless link.end_with?('&') || link.end_with?('?')
62
+ "#{link}page=#{target_page}&per_page=#{per_page}"
63
+ end
64
+
65
+ # Determine the total number of pages
66
+ # --------------------------------------------------------------------------------
67
+ def _page_count(total:, per_page: DEFAULT_PER_PAGE)
68
+ return 1 if total.nil? || per_page.nil? || !total.positive? || !per_page.positive?
69
+
70
+ (total.to_f / per_page).ceil
71
+ end
72
+
73
+ # Remove the pagination query parameters from the URL
74
+ # --------------------------------------------------------------------------------
75
+ def _url_without_pagination(url:)
76
+ return nil if url.nil? || !url.is_a?(String)
77
+
78
+ parts = url.split('?')
79
+ out = parts.first
80
+ query_args = parts.length <= 1 ? [] : parts.last.split('&')
81
+ query_args = query_args.reject { |arg| arg.start_with?('page=') || arg.start_with?('per_page=') }
82
+ return out unless query_args.any?
83
+
84
+ "#{out}?#{query_args.join('&')}"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uc3DmpApiCore
4
+ # Use Rails' ActiveResource to communicate with the DMPHub REST API
5
+ class Responder
6
+ DEFAULT_PAGE = 1
7
+ DEFAULT_PER_PAGE = 25
8
+ MAXIMUM_PER_PAGE = 250
9
+
10
+ DEFAULT_STATUS_CODE = 500
11
+
12
+ TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%L%Z'
13
+
14
+ MSG_INVALID_ARGS = 'Invalid arguments'
15
+
16
+ class << self
17
+ # Standardized Lambda response
18
+ #
19
+ # Expects the following inputs:
20
+ # - status: an HTTP status code (defaults to DEFAULT_STATUS_CODE)
21
+ # - items: an array of Hashes
22
+ # - errors: and array of Strings
23
+ # - args: currently only allows for the Lambda :event
24
+ #
25
+ # Returns a hash that is a valid Lambda API response
26
+ # --------------------------------------------------------------------------------
27
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
28
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
29
+ def respond(status: DEFAULT_STATUS_CODE, items: [], errors: [], **args)
30
+ url = _url_from_event(event: args[:event]) || SsmReader.get_ssm_value(key: 'api_base_url')
31
+ return { statusCode: DEFAULT_STATUS_CODE, body: { errors: ["#{MSG_INVALID_ARGS} - resp"] } } if url.nil?
32
+
33
+ errors = [errors] unless errors.nil? || errors.is_a?(Array)
34
+ item_count = items.is_a?(Array) ? items.length : 0
35
+ page = args[:page] || DEFAULT_PAGE
36
+ per_page = args[:per_page] || DEFAULT_PER_PAGE
37
+
38
+ unless ENV['CORS_ORIGIN'].nil?
39
+ cors_headers = {
40
+ 'Access-Control-Allow-Headers': ENV['CORS_HEADERS'],
41
+ 'Access-Control-Allow-Origin': ENV['CORS_ORIGIN'],
42
+ 'Access-Control-Allow-Methods': ENV['CORS_METHODS']
43
+ }
44
+ end
45
+
46
+ body = {
47
+ status: status.to_i,
48
+ requested: url,
49
+ requested_at: Time.now.strftime(TIMESTAMP_FORMAT),
50
+ total_items: item_count,
51
+ items: items.is_a?(Array) ? items.compact : []
52
+ }
53
+
54
+ body[:errors] = errors if errors.is_a?(Array) && errors.any?
55
+ body = _paginate(url: url, item_count: item_count, body: body, page: page, per_page: per_page)
56
+
57
+ # If this is a server error, then notify the administrator!
58
+ log_error(source: url, message: errors, details: body, event: args[:event]) if status == 500
59
+
60
+ { statusCode: status.to_i, body: body.to_json, headers: cors_headers.nil? ? {} : cors_headers }
61
+ rescue StandardError => e
62
+ puts "LambdaLayer: Responder.respond - #{e.message}"
63
+ puts " - STACK: #{e.backtrace}"
64
+ { statusCode: DEFAULT_STATUS_CODE, body: { errors: ["#{MSG_INVALID_ARGS} - resp err"] } }
65
+ end
66
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
67
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
+
69
+ private
70
+
71
+ # Figure out the requested URL from the Lambda event hash
72
+ # --------------------------------------------------------------------------------
73
+ def _url_from_event(event:)
74
+ return '' unless event.is_a?(Hash)
75
+
76
+ url = event.fetch('path', '/')
77
+ return url if event['queryStringParameters'].nil?
78
+
79
+ "#{url}?#{event['queryStringParameters'].map { |k, v| "#{k}=#{v}" }.join('&')}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-ssm'
4
+
5
+ module Uc3DmpApiCore
6
+ # ----------------------------------------------------
7
+ # SSM Parameter Store Helper
8
+ #
9
+ # Shared helper methods for accessing SSM parameters
10
+ # ----------------------------------------------------
11
+ class SsmReader
12
+ SOURCE = 'SsmReader gem'
13
+
14
+ class << self
15
+ # Return all of the available keys
16
+ def available_keys
17
+ _ssm_keys.keys
18
+ end
19
+
20
+ # Fetch the value for the specified :key
21
+ # ----------------------------------------------------
22
+ # rubocop:disable Metrics/AbcSize
23
+ def get_ssm_value(key:, provenance_name: nil)
24
+ full_key = _ssm_keys[:"#{key.downcase.to_s}"] unless key.nil?
25
+ return nil if full_key.nil?
26
+
27
+
28
+ key_vals = { env: ENV.fetch('LAMBDA_ENV', 'dev').to_s.downcase }
29
+ # Swap in the provenance name if applicable
30
+ key_vals[:provenance] = provenance_name unless provenance_name.nil? ||
31
+ !full_key.include?('%{provenance}')
32
+ fetch_value(key: format(full_key, key_vals))
33
+ rescue Aws::Errors::ServiceError => e
34
+ Logger.log_error(
35
+ source: "#{SOURCE} - looking for #{key}", message: e.message, details: e.backtrace
36
+ )
37
+ nil
38
+ end
39
+ # rubocop:enable Metrics/AbcSize
40
+
41
+ # Call SSM to get the value for the specified key
42
+ def fetch_value(key:)
43
+ resp = Aws::SSM::Client.new.get_parameter(name: key, with_decryption: true)
44
+ resp.nil? || resp.parameter.nil? ? nil : resp.parameter.value
45
+ end
46
+
47
+ # Checks to see if debug mode has been enabled in SSM
48
+ # ----------------------------------------------------
49
+ def debug_mode?
50
+ get_ssm_value(key: _ssm_keys[:debug_mode])&.downcase&.strip == 'true'
51
+ end
52
+
53
+ private
54
+
55
+ # DMPTool/DMPHub SSM keys. See the installation guide for information about how these values are used
56
+ # https://github.com/CDLUC3/dmp-hub-cfn/wiki/installation-and-setup#required-ssm-parameters
57
+ def _ssm_keys
58
+ {
59
+ administrator_email: '/uc3/dmp/hub/%{env}/AdminEmail',
60
+ api_base_url: '/uc3/dmp/hub/%{env}/ApiBaseUrl',
61
+ base_url: '/uc3/dmp/hub/%{env}/BaseUrl',
62
+ debug_mode: '/uc3/dmp/hub/%{env}/Debug',
63
+
64
+ dmp_id_api_url: '/uc3/dmp/hub/%{env}/EzidApiUrl',
65
+ dmp_id_base_url: '/uc3/dmp/hub/%{env}/EzidBaseUrl',
66
+ dmp_id_client_id: '/uc3/dmp/hub/%{env}/EzidUsername',
67
+ dmp_id_client_name: '/uc3/dmp/hub/%{env}/EzidHostingInstitution',
68
+ dmp_id_client_secret: '/uc3/dmp/hub/%{env}/EzidPassword',
69
+ dmp_id_debug_mode: '/uc3/dmp/hub/%{env}/EzidDebugMode',
70
+ dmp_id_paused: '/uc3/dmp/hub/%{env}/EzidPaused',
71
+ dmp_id_shoulder: '/uc3/dmp/hub/%{env}/EzidShoulder',
72
+
73
+ provenance_api_client_id: '/uc3/dmp/hub/%{env}/%{provenance}/client_id',
74
+ provenance_api_client_secret: '/uc3/dmp/hub/%{env}/%{provenance}/client_secret',
75
+
76
+ s3_bucket_url: '/uc3/dmp/hub/%{env}/S3CloudFrontBucketUrl',
77
+ s3_access_point: '/uc3/dmp/hub/%{env}/S3CloudFrontBucketAccessPoint',
78
+
79
+ rds_username: '/uc3/dmp/tool/%{env}/RdsUsername',
80
+ rds_pasword: '/uc3/dmp/tool/%{env}/RdsPassword',
81
+
82
+ dynamo_table_name: '/uc3/dmp/hub/%{env}/DynamoTableName'
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uc3DmpApiCore
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+ require 'aws-sdk-ssm'
5
+
6
+ require 'uc3-dmp-api-core/notifier'
7
+ require 'uc3-dmp-api-core/paginator'
8
+ require 'uc3-dmp-api-core/responder'
9
+ require 'uc3-dmp-api-core/ssm_reader'
10
+
11
+ module Uc3DmpApiCore
12
+ # General HTTP Response Messages
13
+ # ----------------------------------------
14
+ MSG_SUCCESS = 'Success'
15
+ MSG_INVALID_ARGS = 'Invalid arguments.' # For HTTP 400 (Bad request)
16
+ MSG_SERVER_ERROR = 'Unable to process your request at this time.' # For HTTP 500 (Server error)
17
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uc3-dmp-api-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Brian Riley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-sns
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.60'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.60'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk-ssm
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.150'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.150'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 11.1.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 11.1.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 3.9.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 3.9.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 0.88.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 0.88.0
111
+ description: Helpers for SSM, EventBridge, standardizing responses/errors
112
+ email:
113
+ - brian.riley@ucop.edu
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - README.md
119
+ - lib/uc3-dmp-api-core.rb
120
+ - lib/uc3-dmp-api-core/logger.rb
121
+ - lib/uc3-dmp-api-core/notifier.rb
122
+ - lib/uc3-dmp-api-core/paginator.rb
123
+ - lib/uc3-dmp-api-core/responder.rb
124
+ - lib/uc3-dmp-api-core/ssm_reader.rb
125
+ - lib/uc3-dmp-api-core/version.rb
126
+ homepage: https://github.com/CDLUC3/dmp-hub-cfn/blob/main/src/sam/gems/uc3-dmp-api-core
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '2.7'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.1.6
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: DMPTool gem that provides general support for Lambda functions
149
+ test_files: []