uc3-dmp-api-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []