lambda-microvms 0.0.0 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de35523913c4cd5c11fc48549081c7901db9fdacfef2a72618264885596bb838
4
- data.tar.gz: 960ec7995dc496da3f358c6ad484eb0a420140afa727275b92e5eadf7201ce2b
3
+ metadata.gz: 296c088486e240241e9e84b8932be4b07dd929aa4168b3b88d96347c5f6e9173
4
+ data.tar.gz: 8b3b9880d86869ef123247fb635d3a09bbc812bc6512ea6a45e6983c2414c04a
5
5
  SHA512:
6
- metadata.gz: 055d951f5d0a5dc948195eff437d7b67070faf74e5dba4eee65b583aa70fda29a0452d93ecbe4e311bf3e7f5f18054d999776c6c5294bc2c88b061c8a777c77c
7
- data.tar.gz: 6948f65e7391fa14734dcd1f89757bfb6a1b2a04ee200566409de0851da3a80f41a7173607cbd6efa40a5bc31d09d1033237512eb145bf23358459871dd77da5
6
+ metadata.gz: 114c31806d2c30c3682b9f33a79ddc2b3b114a5c4898f48f1fa07edf5ecf5e61bdd2cdc9d49150ca415715bb8e1a911335bd44a59f6277adb8e50ca424235a23
7
+ data.tar.gz: 713a3dabdfeac0e9dad4ea3212fa556295e6b77ee9929daf919ec56c6f01c8cce1e384220548a17fff6bb2e900ffe4903a21650edcfbe7929f7400b420c8eaee
data/README.md CHANGED
@@ -177,3 +177,4 @@ Lambda::MicroVMs::Client
177
177
  ## Status
178
178
 
179
179
  This is an early implementation. Lambda MicroVMs is new, so generated AWS SDK operation shapes may evolve. Unsupported low-level operations raise `Lambda::MicroVMs::UnsupportedOperationError` with an upgrade hint.
180
+ give
data/exe/lambda-microvms CHANGED
@@ -23,6 +23,7 @@ module Lambda
23
23
  when 'deploy' then deploy
24
24
  when 'run' then run_microvm
25
25
  when 'doctor' then doctor
26
+ when 'sdk-contract' then sdk_contract
26
27
  when 'help', nil then help
27
28
  else
28
29
  @err.puts "unknown command: #{command}"
@@ -81,6 +82,17 @@ module Lambda
81
82
  checks.all?(&:ok) ? 0 : 1
82
83
  end
83
84
 
85
+ def sdk_contract
86
+ unsupported = Client.unsupported_operations
87
+ if unsupported.empty?
88
+ @out.puts 'Aws::Lambda::Client exposes all Lambda MicroVM operations'
89
+ 0
90
+ else
91
+ @err.puts "missing Aws::Lambda::Client operations: #{unsupported.join(', ')}"
92
+ 1
93
+ end
94
+ end
95
+
84
96
  def help
85
97
  @out.puts <<~HELP
86
98
  lambda-microvms #{VERSION}
@@ -91,6 +103,7 @@ module Lambda
91
103
  package Create deployable source artifact
92
104
  deploy Package, upload to S3, and create a MicroVM image
93
105
  run Run the configured MicroVM image
106
+ sdk-contract Check whether aws-sdk-lambda exposes MicroVM operations
94
107
  version Print version
95
108
  HELP
96
109
  0
@@ -10,66 +10,153 @@ module Lambda
10
10
  module MicroVMs
11
11
  # Ruby wrapper over Aws::Lambda::Client for Lambda MicroVM lifecycle operations.
12
12
  class Client
13
+ # SDK methods this wrapper expects from Aws::Lambda::Client.
14
+ REQUIRED_OPERATIONS = %i[
15
+ create_microvm_image
16
+ get_microvm_image
17
+ delete_microvm_image
18
+ run_microvm
19
+ get_microvm
20
+ list_microvms
21
+ suspend_microvm
22
+ resume_microvm
23
+ terminate_microvm
24
+ create_microvm_auth_token
25
+ ].freeze
26
+
13
27
  attr_reader :sdk
14
28
 
15
29
  def initialize(region: nil, profile: nil, sdk: nil, **)
16
- @sdk = sdk || build_sdk(region:, profile:, **)
30
+ @sdk = sdk || build_sdk(region: region, profile: profile, **)
31
+ end
32
+
33
+ # Return wrapper operations missing from the given SDK client.
34
+ #
35
+ # @param sdk [Object, nil] SDK client to inspect
36
+ # @return [Array<Symbol>] missing SDK operation names
37
+ def self.unsupported_operations(sdk = nil)
38
+ sdk ||= Aws::Lambda::Client.new(stub_responses: true) if defined?(Aws::Lambda::Client)
39
+ return REQUIRED_OPERATIONS unless sdk
40
+
41
+ REQUIRED_OPERATIONS.reject { |operation| sdk.respond_to?(operation) }
42
+ end
43
+
44
+ # Check whether the given SDK client exposes all MicroVM operations.
45
+ #
46
+ # @param sdk [Object, nil] SDK client to inspect
47
+ # @return [Boolean]
48
+ def self.sdk_contract_supported?(sdk = nil)
49
+ unsupported_operations(sdk).empty?
17
50
  end
18
51
 
52
+ # Build an image resource wrapper without fetching it.
53
+ #
54
+ # @param arn [String] MicroVM image ARN
55
+ # @return [Image] image resource wrapper
19
56
  def image(arn)
20
57
  Image.new(client: self, arn: arn)
21
58
  end
22
59
 
60
+ # Build a MicroVM resource wrapper without fetching it.
61
+ #
62
+ # @param id_or_arn [String] MicroVM id or ARN
63
+ # @return [MicroVM] MicroVM resource wrapper
23
64
  def microvm(id_or_arn)
24
65
  MicroVM.new(client: self, id: id_or_arn)
25
66
  end
26
67
 
68
+ # Create a MicroVM image through the Lambda SDK.
69
+ #
70
+ # @param params [Hash] SDK request parameters
71
+ # @return [Image] created image resource
27
72
  def create_image(**params)
28
73
  response = call_sdk(:create_microvm_image, **params)
29
74
  Image.from_response(client: self, response: response)
30
75
  end
31
76
 
77
+ # Fetch a MicroVM image and wrap the SDK response.
78
+ #
79
+ # @param params [Hash] SDK request parameters
80
+ # @return [Image] fetched image resource
32
81
  def get_image(**params)
33
82
  response = call_sdk(:get_microvm_image, **params)
34
83
  Image.from_response(client: self, response: response)
35
84
  end
36
85
 
86
+ # Delete a MicroVM image.
87
+ #
88
+ # @param params [Hash] SDK request parameters
89
+ # @return [Object] raw SDK response
37
90
  def delete_image(**params)
38
91
  call_sdk(:delete_microvm_image, **params)
39
92
  end
40
93
 
94
+ # Run a MicroVM from an image.
95
+ #
96
+ # @param params [Hash] SDK request parameters
97
+ # @return [MicroVM] started MicroVM resource
41
98
  def run(**params)
42
99
  response = call_sdk(:run_microvm, **params)
43
100
  MicroVM.from_response(client: self, response: response)
44
101
  end
45
102
  alias run_microvm run
46
103
 
104
+ # Fetch a MicroVM and wrap the SDK response.
105
+ #
106
+ # @param params [Hash] SDK request parameters
107
+ # @return [MicroVM] fetched MicroVM resource
47
108
  def get_microvm(**params)
48
109
  response = call_sdk(:get_microvm, **params)
49
110
  MicroVM.from_response(client: self, response: response)
50
111
  end
51
112
 
113
+ # List MicroVMs using the underlying Lambda SDK client.
114
+ #
115
+ # @param params [Hash] SDK request parameters
116
+ # @return [Object] raw SDK response
52
117
  def list_microvms(**params)
53
118
  call_sdk(:list_microvms, **params)
54
119
  end
55
120
 
121
+ # Suspend a MicroVM.
122
+ #
123
+ # @param params [Hash] SDK request parameters
124
+ # @return [Object] raw SDK response
56
125
  def suspend_microvm(**params)
57
126
  call_sdk(:suspend_microvm, **params)
58
127
  end
59
128
 
129
+ # Resume a suspended MicroVM.
130
+ #
131
+ # @param params [Hash] SDK request parameters
132
+ # @return [Object] raw SDK response
60
133
  def resume_microvm(**params)
61
134
  call_sdk(:resume_microvm, **params)
62
135
  end
63
136
 
137
+ # Terminate a MicroVM.
138
+ #
139
+ # @param params [Hash] SDK request parameters
140
+ # @return [Object] raw SDK response
64
141
  def terminate_microvm(**params)
65
142
  call_sdk(:terminate_microvm, **params)
66
143
  end
67
144
 
145
+ # Create an auth token for direct MicroVM endpoint access.
146
+ #
147
+ # @param params [Hash] SDK request parameters
148
+ # @return [Object] raw SDK response
68
149
  def create_auth_token(**params)
69
150
  call_sdk(:create_microvm_auth_token, **params)
70
151
  end
71
152
  alias create_microvm_auth_token create_auth_token
72
153
 
154
+ # Dispatch a supported operation to the underlying SDK client.
155
+ #
156
+ # @param operation [Symbol] SDK method name
157
+ # @param params [Hash] SDK request parameters
158
+ # @return [Object] raw SDK response
159
+ # @raise [UnsupportedOperationError] when the SDK does not expose the operation
73
160
  def call_sdk(operation, **params)
74
161
  unless @sdk.respond_to?(operation)
75
162
  raise UnsupportedOperationError, "Aws::Lambda::Client does not expose ##{operation}; upgrade aws-sdk-lambda"
@@ -15,28 +15,41 @@ module Lambda
15
15
  attr_reader :project, :client, :s3
16
16
 
17
17
  def initialize(project:, client: nil, s3: nil) # rubocop:disable Naming/MethodParameterName
18
- @project = project
18
+ @project = project.validate!
19
19
  @client = client || Client.new(region: project.region, profile: project.profile)
20
20
  @s3 = s3 || build_s3
21
21
  end
22
22
 
23
+ # Package the project into a deployable zip artifact.
24
+ #
25
+ # @return [String] artifact path
23
26
  def package
24
27
  Packager.new(project).package
25
28
  end
26
29
 
30
+ # Upload an artifact to the configured S3 bucket and prefix.
31
+ #
32
+ # @param path [String] local artifact path
33
+ # @return [String] S3 URI
27
34
  def upload(path)
28
35
  bucket = project.require!('deployment.bucket', project.s3_bucket)
29
36
  key = [project.s3_prefix.sub(%r{/\z}, ''), File.basename(path)].join('/')
30
- s3.put_object(bucket: bucket, key: key, body: File.open(path, 'rb'))
37
+ File.open(path, 'rb') { |body| s3.put_object(bucket: bucket, key: key, body: body) }
31
38
  "s3://#{bucket}/#{key}"
32
39
  end
33
40
 
41
+ # Package, upload, and create a MicroVM image.
42
+ #
43
+ # @return [Image] created image resource
34
44
  def deploy
35
45
  artifact = package
36
46
  artifact_uri = upload(artifact)
37
47
  client.create_image(**project.create_image_params(artifact_uri: artifact_uri))
38
48
  end
39
49
 
50
+ # Run the configured image with configured runtime parameters.
51
+ #
52
+ # @return [MicroVM] started MicroVM resource
40
53
  def run
41
54
  image_arn = project.require!('image.arn', project.image_arn)
42
55
  role_arn = project.require!('role_arn', project.role_arn)
@@ -48,9 +61,7 @@ module Lambda
48
61
  def build_s3
49
62
  raise LoadError, 'install aws-sdk-s3 to use deployment helpers' unless defined?(Aws::S3::Client)
50
63
 
51
- args = {}
52
- args[:region] = project.region if project.region
53
- args[:profile] = project.profile if project.profile
64
+ args = { region: project.region, profile: project.profile }.compact
54
65
  Aws::S3::Client.new(**args)
55
66
  end
56
67
  end
@@ -5,6 +5,7 @@ module Lambda
5
5
  module MicroVMs
6
6
  # Performs lightweight local project readiness checks.
7
7
  class Doctor
8
+ # Result object for a single doctor check.
8
9
  Check = Struct.new(:name, :ok, :detail, keyword_init: true)
9
10
 
10
11
  attr_reader :project, :runner
@@ -14,6 +15,9 @@ module Lambda
14
15
  @runner = runner
15
16
  end
16
17
 
18
+ # Run all local readiness checks.
19
+ #
20
+ # @return [Array<Check>] check results
17
21
  def checks
18
22
  [
19
23
  check('Ruby', RUBY_VERSION >= '3.2', RUBY_VERSION),
@@ -49,7 +53,8 @@ module Lambda
49
53
  end
50
54
 
51
55
  def config_check(name, value)
52
- Check.new(name:, ok: value && value != '', detail: value || 'missing')
56
+ ok = value && value.to_s.strip != ''
57
+ Check.new(name:, ok: ok, detail: ok ? value : 'missing')
53
58
  end
54
59
 
55
60
  def ric_check
@@ -16,17 +16,39 @@ module Lambda
16
16
  @http = http
17
17
  end
18
18
 
19
+ # Send an authenticated GET request to the MicroVM endpoint.
20
+ #
21
+ # @param path [String] endpoint path
22
+ # @param headers [Hash] additional HTTP headers
23
+ # @return [Hash,String,nil] parsed JSON response or raw body
19
24
  def get(path, headers: {})
20
25
  request(Net::HTTP::Get, path, headers:)
21
26
  end
22
27
 
28
+ # Send an authenticated POST request to the MicroVM endpoint.
29
+ #
30
+ # @param path [String] endpoint path
31
+ # @param json [Hash,Array,nil] JSON body to encode
32
+ # @param body [String,nil] raw request body
33
+ # @param headers [Hash] additional HTTP headers
34
+ # @return [Hash,String,nil] parsed JSON response or raw body
23
35
  def post(path, json: nil, body: nil, headers: {})
24
36
  request(Net::HTTP::Post, path, json:, body:, headers:)
25
37
  end
26
38
 
39
+ # Build and execute an authenticated HTTP request.
40
+ #
41
+ # @param klass [Class] Net::HTTP request class
42
+ # @param path [String] endpoint path
43
+ # @param json [Hash,Array,nil] JSON body to encode
44
+ # @param body [String,nil] raw request body
45
+ # @param headers [Hash] additional HTTP headers
46
+ # @return [Hash,String,nil] parsed JSON response or raw body
47
+ # @raise [EndpointError] when the endpoint returns a non-2xx status
27
48
  def request(klass, path, json: nil, body: nil, headers: {})
28
- response = @http.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
29
- http.request(build_request(klass, path, headers:, body:, json:))
49
+ req = build_request(klass, path, headers:, body:, json:)
50
+ response = @http.start(req.uri.host, req.uri.port, use_ssl: req.uri.scheme == 'https') do |http|
51
+ http.request(req)
30
52
  end
31
53
 
32
54
  success = response.code.to_i.between?(200, 299)
@@ -41,10 +63,10 @@ module Lambda
41
63
  private
42
64
 
43
65
  def build_uri(path)
44
- base = URI(@url)
66
+ base = ::URI.parse(@url)
45
67
  return base if path.nil? || path.empty? || path == '/'
46
68
 
47
- joined = [base.path.sub(%r{/\z}, ''), path.sub(%r{\A/}, '')].reject(&:empty?).join('/')
69
+ joined = [base.path.sub(%r{/\z}, ''), path.sub(%r{\A/}, '')].reject(&:empty?).join('/').sub(%r{\A/+}, '')
48
70
  base.path = "/#{joined}"
49
71
  base
50
72
  end
@@ -6,6 +6,11 @@ module Lambda
6
6
  class Image
7
7
  attr_reader :client, :arn, :data
8
8
 
9
+ # Build an image resource from an SDK response.
10
+ #
11
+ # @param client [Client] lifecycle client
12
+ # @param response [Object] SDK response
13
+ # @return [Image] image resource
9
14
  def self.from_response(client:, response:)
10
15
  arn = Util.extract(response, :image_arn, :microvm_image_arn, :arn)
11
16
  new(client: client, arn: arn, data: response)
@@ -17,12 +22,18 @@ module Lambda
17
22
  @data = data
18
23
  end
19
24
 
25
+ # Refresh image data from the service.
26
+ #
27
+ # @return [self]
20
28
  def refresh
21
29
  fresh = client.get_image(image_arn: arn)
22
30
  @data = fresh.data
23
31
  self
24
32
  end
25
33
 
34
+ # Current normalized image state.
35
+ #
36
+ # @return [Symbol]
26
37
  def state
27
38
  Util.normalize_state(Util.extract(@data, :state, :status, :image_state))
28
39
  end
@@ -31,6 +42,11 @@ module Lambda
31
42
  %i[ready available active].include?(state)
32
43
  end
33
44
 
45
+ # Wait until the image reaches a ready-like state.
46
+ #
47
+ # @param delay [Numeric] polling delay in seconds
48
+ # @param timeout [Numeric] maximum wait in seconds
49
+ # @return [self]
34
50
  def wait_until_ready(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
35
51
  Waiter.new(delay: delay, timeout: timeout).wait(message: "image #{arn} to be ready") do
36
52
  refresh.ready?
@@ -38,6 +54,12 @@ module Lambda
38
54
  self
39
55
  end
40
56
 
57
+ # Run a MicroVM from this image.
58
+ #
59
+ # @param role_arn [String] IAM role ARN used by the MicroVM runtime
60
+ # @param payload [Object,nil] optional runtime payload
61
+ # @param params [Hash] additional run parameters
62
+ # @return [MicroVM]
41
63
  def run(role_arn:, payload: nil, **params)
42
64
  request = params.merge(image_arn: arn, role_arn: role_arn)
43
65
  request[:payload] = payload if payload
@@ -6,6 +6,11 @@ module Lambda
6
6
  class MicroVM
7
7
  attr_reader :client, :id, :data
8
8
 
9
+ # Build a MicroVM resource from an SDK response.
10
+ #
11
+ # @param client [Client] lifecycle client
12
+ # @param response [Object] SDK response
13
+ # @return [MicroVM] MicroVM resource
9
14
  def self.from_response(client:, response:)
10
15
  id = Util.extract(response, :microvm_id, :microvm_arn, :id, :arn)
11
16
  new(client: client, id: id, data: response)
@@ -17,12 +22,18 @@ module Lambda
17
22
  @data = data
18
23
  end
19
24
 
25
+ # Refresh MicroVM data from the service.
26
+ #
27
+ # @return [self]
20
28
  def refresh
21
29
  fresh = client.get_microvm(microvm_id: id)
22
30
  @data = fresh.data
23
31
  self
24
32
  end
25
33
 
34
+ # Current normalized MicroVM state.
35
+ #
36
+ # @return [Symbol]
26
37
  def state
27
38
  Util.normalize_state(Util.extract(@data, :state, :status, :microvm_state))
28
39
  end
@@ -43,6 +54,9 @@ module Lambda
43
54
  state == :pending
44
55
  end
45
56
 
57
+ # Extract the direct endpoint URL from the current MicroVM data.
58
+ #
59
+ # @return [String, nil]
46
60
  def endpoint_url
47
61
  endpoint = Util.extract(@data, :endpoint, :endpoint_url, :url)
48
62
  return endpoint if endpoint.is_a?(String)
@@ -50,6 +64,12 @@ module Lambda
50
64
  Util.extract(endpoint, :url, :endpoint_url) if endpoint
51
65
  end
52
66
 
67
+ # Request an auth token for direct endpoint access.
68
+ #
69
+ # @param ports [Array<Integer>, nil] optional allowed ports
70
+ # @param ttl_seconds [Integer, nil] optional token lifetime
71
+ # @param params [Hash] additional token request parameters
72
+ # @return [String, nil] auth token value
53
73
  def auth_token(ports: nil, ttl_seconds: nil, **params)
54
74
  request = params.merge(microvm_id: id)
55
75
  request[:ports] = ports if ports
@@ -58,34 +78,71 @@ module Lambda
58
78
  Util.extract(response, :token, :auth_token, :microvm_auth_token)
59
79
  end
60
80
 
81
+ # Build an endpoint client for this MicroVM.
82
+ #
83
+ # @param token [String, nil] existing auth token
84
+ # @param token_params [Hash] parameters used when requesting a token
85
+ # @return [Endpoint]
61
86
  def endpoint(token: nil, **token_params)
62
- Endpoint.new(url: endpoint_url, token: token || auth_token(**token_params))
87
+ url = endpoint_url
88
+ raise EndpointError.new('MicroVM endpoint URL is unavailable', status: 0, body: nil) unless url
89
+
90
+ Endpoint.new(url: url, token: token || auth_token(**token_params))
63
91
  end
64
92
 
93
+ # Send a GET request to the MicroVM endpoint.
94
+ #
95
+ # @param path [String] endpoint path
96
+ # @param token [String, nil] existing auth token
97
+ # @return [Hash,String,nil] endpoint response
65
98
  def get(path, token: nil, **)
66
99
  endpoint(token: token).get(path, **)
67
100
  end
68
101
 
102
+ # Send a POST request to the MicroVM endpoint.
103
+ #
104
+ # @param path [String] endpoint path
105
+ # @param json [Hash,Array,nil] JSON body to encode
106
+ # @param body [String,nil] raw request body
107
+ # @param token [String, nil] existing auth token
108
+ # @return [Hash,String,nil] endpoint response
69
109
  def post(path, json: nil, body: nil, token: nil, **)
70
110
  endpoint(token: token).post(path, json: json, body: body, **)
71
111
  end
72
112
 
113
+ # Suspend this MicroVM.
114
+ #
115
+ # @param params [Hash] additional suspend parameters
116
+ # @return [self]
73
117
  def suspend(**params)
74
118
  client.suspend_microvm(**params, microvm_id: id)
75
119
  self
76
120
  end
77
121
 
122
+ # Resume this MicroVM and update local data when the service returns a response.
123
+ #
124
+ # @param params [Hash] additional resume parameters
125
+ # @return [self]
78
126
  def resume(**params)
79
127
  response = client.resume_microvm(**params, microvm_id: id)
80
128
  @data = response if response
81
129
  self
82
130
  end
83
131
 
132
+ # Terminate this MicroVM.
133
+ #
134
+ # @param params [Hash] additional terminate parameters
135
+ # @return [self]
84
136
  def terminate(**params)
85
137
  client.terminate_microvm(**params, microvm_id: id)
86
138
  self
87
139
  end
88
140
 
141
+ # Wait until this MicroVM is running.
142
+ #
143
+ # @param delay [Numeric] polling delay in seconds
144
+ # @param timeout [Numeric] maximum wait in seconds
145
+ # @return [self]
89
146
  def wait_until_running(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
90
147
  Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be running") do
91
148
  refresh.running?
@@ -93,6 +150,11 @@ module Lambda
93
150
  self
94
151
  end
95
152
 
153
+ # Wait until this MicroVM is suspended.
154
+ #
155
+ # @param delay [Numeric] polling delay in seconds
156
+ # @param timeout [Numeric] maximum wait in seconds
157
+ # @return [self]
96
158
  def wait_until_suspended(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
97
159
  Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be suspended") do
98
160
  refresh.suspended?
@@ -100,6 +162,11 @@ module Lambda
100
162
  self
101
163
  end
102
164
 
165
+ # Wait until this MicroVM is terminated.
166
+ #
167
+ # @param delay [Numeric] polling delay in seconds
168
+ # @param timeout [Numeric] maximum wait in seconds
169
+ # @return [self]
103
170
  def wait_until_terminated(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
104
171
  Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be terminated") do
105
172
  refresh.terminated?
@@ -8,14 +8,21 @@ module Lambda
8
8
  module MicroVMs
9
9
  # Creates a deployable source artifact for Lambda MicroVM image creation.
10
10
  class Packager
11
+ # Default zip exclusion patterns.
11
12
  DEFAULT_EXCLUDES = ['.git/*', 'tmp/*', 'vendor/bundle/*', '*.gem'].freeze
12
13
 
13
14
  attr_reader :project
14
15
 
15
- def initialize(project)
16
+ def initialize(project, runner: Open3)
16
17
  @project = project
18
+ @runner = runner
17
19
  end
18
20
 
21
+ # Create the zip artifact for the configured project.
22
+ #
23
+ # @param output [String] output zip path
24
+ # @return [String] output path
25
+ # @raise [CommandError] when zip exits unsuccessfully
19
26
  def package(output: project.artifact_path)
20
27
  FileUtils.mkdir_p(File.dirname(output))
21
28
  relative_output = begin
@@ -25,7 +32,7 @@ module Lambda
25
32
  end
26
33
  excludes = DEFAULT_EXCLUDES + [relative_output]
27
34
  command = ['zip', '-q', '-r', output, '.'] + excludes.flat_map { |pattern| ['-x', pattern] }
28
- stdout, stderr, status = Open3.capture3(*command, chdir: project.root)
35
+ stdout, stderr, status = @runner.capture3(*command, chdir: project.root)
29
36
  raise CommandError, "zip failed: #{stderr.empty? ? stdout : stderr}" unless status.success?
30
37
 
31
38
  output
@@ -8,10 +8,15 @@ module Lambda
8
8
  module MicroVMs
9
9
  # Reads lambda-microvms project configuration from microvm.yml.
10
10
  class Project
11
+ # Default project configuration file name.
11
12
  DEFAULT_CONFIG = 'microvm.yml'
12
13
 
13
14
  attr_reader :root, :config_path, :config
14
15
 
16
+ # Load a project configuration from disk.
17
+ #
18
+ # @param path [String] path to a YAML configuration file
19
+ # @return [Project] loaded project
15
20
  def self.load(path = DEFAULT_CONFIG)
16
21
  config_path = File.expand_path(path)
17
22
  root = File.dirname(config_path)
@@ -25,36 +30,79 @@ module Lambda
25
30
  @config = stringify_keys(config)
26
31
  end
27
32
 
33
+ # Project name.
34
+ #
35
+ # @return [String]
28
36
  def name = fetch('name', default: File.basename(root))
29
37
 
38
+ # AWS region from configuration or environment.
39
+ #
40
+ # @return [String, nil]
30
41
  def region = fetch('region', default: ENV.fetch('AWS_REGION', nil))
31
42
 
43
+ # AWS profile from configuration or environment.
44
+ #
45
+ # @return [String, nil]
32
46
  def profile = fetch('profile', default: ENV.fetch('AWS_PROFILE', nil))
33
47
 
48
+ # IAM role ARN used when running MicroVMs.
49
+ #
50
+ # @return [String, nil]
34
51
  def role_arn = fetch('role_arn', 'role', default: nil)
35
52
 
53
+ # Existing MicroVM image ARN.
54
+ #
55
+ # @return [String, nil]
36
56
  def image_arn = fetch('image_arn', 'image.arn', default: nil)
37
57
 
58
+ # Name used when creating a MicroVM image.
59
+ #
60
+ # @return [String]
38
61
  def image_name = fetch('image.name', default: name)
39
62
 
63
+ # Absolute path to the Dockerfile.
64
+ #
65
+ # @return [String]
40
66
  def dockerfile = File.expand_path(fetch('build.dockerfile', default: 'Dockerfile'), root)
41
67
 
68
+ # Absolute path to the build context.
69
+ #
70
+ # @return [String]
42
71
  def build_context = File.expand_path(fetch('build.context', default: '.'), root)
43
72
 
73
+ # Absolute path for the packaged artifact.
74
+ #
75
+ # @return [String]
44
76
  def artifact_path = File.expand_path(fetch('build.artifact', default: "tmp/#{name}-microvm.zip"), root)
45
77
 
78
+ # S3 bucket used for deployment artifacts.
79
+ #
80
+ # @return [String, nil]
46
81
  def s3_bucket = fetch('deployment.bucket', 's3.bucket', default: nil)
47
82
 
83
+ # S3 key prefix used for deployment artifacts.
84
+ #
85
+ # @return [String]
48
86
  def s3_prefix = fetch('deployment.prefix', 's3.prefix', default: "lambda-microvms/#{name}")
49
87
 
88
+ # Cleanup policy used after a session block.
89
+ #
90
+ # @return [Symbol]
50
91
  def lifecycle_after = fetch('runtime.after', default: 'suspend').to_sym
51
92
 
93
+ # Runtime payload configured for MicroVM runs.
94
+ #
95
+ # @return [Hash]
52
96
  def payload
53
- fetch('runtime.payload', default: {}) || {}
97
+ hash_config('runtime.payload')
54
98
  end
55
99
 
100
+ # Build parameters for image creation with the artifact URI injected.
101
+ #
102
+ # @param artifact_uri [String] uploaded artifact URI
103
+ # @return [Hash] SDK-style create image parameters
56
104
  def create_image_params(artifact_uri:)
57
- params = fetch('image.create', default: {}) || {}
105
+ params = hash_config('image.create')
58
106
  params = stringify_keys(params)
59
107
  symbolized = params.to_h { |key, value| [key.to_sym, value] }
60
108
  symbolized[:name] ||= image_name
@@ -62,22 +110,63 @@ module Lambda
62
110
  symbolized
63
111
  end
64
112
 
113
+ # Build parameters for running a MicroVM.
114
+ #
115
+ # @return [Hash] SDK-style run parameters
65
116
  def run_params
66
- params = stringify_keys(fetch('runtime.run', default: {}) || {})
117
+ params = stringify_keys(hash_config('runtime.run'))
67
118
  symbolized = params.to_h { |key, value| [key.to_sym, value] }
68
119
  symbolized[:role_arn] ||= role_arn if role_arn
69
120
  symbolized[:payload] ||= payload unless payload.empty?
70
121
  symbolized
71
122
  end
72
123
 
124
+ # Validate project configuration sections used by deployment commands.
125
+ #
126
+ # @return [self]
127
+ # @raise [ConfigurationError] when a known section has an invalid shape
128
+ def validate!
129
+ validate_hash!
130
+ validate_lifecycle_after!
131
+ hash_config('image.create')
132
+ hash_config('runtime.payload')
133
+ hash_config('runtime.run')
134
+ self
135
+ end
136
+
137
+ # Return a required value or raise a configuration error.
138
+ #
139
+ # @param field [String] configuration field name used in the error
140
+ # @param value [Object] value to validate
141
+ # @return [Object] the validated value
142
+ # @raise [ConfigurationError] when the value is nil or empty
73
143
  def require!(field, value)
74
- return value if value && value != ''
144
+ return value if value && value.to_s.strip != ''
75
145
 
76
146
  raise ConfigurationError, "missing required project configuration: #{field}"
77
147
  end
78
148
 
79
149
  private
80
150
 
151
+ def validate_hash!
152
+ return if @config.is_a?(Hash)
153
+
154
+ raise ConfigurationError, 'project configuration must be a YAML mapping'
155
+ end
156
+
157
+ def validate_lifecycle_after!
158
+ return if %i[keep suspend terminate].include?(lifecycle_after)
159
+
160
+ raise ConfigurationError, "unsupported runtime.after: #{lifecycle_after.inspect}"
161
+ end
162
+
163
+ def hash_config(path)
164
+ value = fetch(path, default: {}) || {}
165
+ return value if value.is_a?(Hash)
166
+
167
+ raise ConfigurationError, "#{path} must be a mapping"
168
+ end
169
+
81
170
  def fetch(*paths, default:)
82
171
  paths.each do |path|
83
172
  current = @config
@@ -14,6 +14,9 @@ module Lambda
14
14
  @force = force
15
15
  end
16
16
 
17
+ # Create the scaffolded project files.
18
+ #
19
+ # @return [String] target directory
17
20
  def create
18
21
  ensure_target!
19
22
  FileUtils.mkdir_p(directory)
@@ -6,6 +6,15 @@ module Lambda
6
6
  module Session
7
7
  module_function
8
8
 
9
+ # Run a MicroVM, wait for it, yield it, and then clean it up.
10
+ #
11
+ # @param image_arn [String] MicroVM image ARN
12
+ # @param role_arn [String] IAM role ARN for the MicroVM runtime
13
+ # @param after [Symbol, nil] cleanup policy: :suspend, :terminate, :keep, or nil
14
+ # @param client [Client] lifecycle client
15
+ # @param run_options [Hash] additional run parameters
16
+ # @yieldparam vm [MicroVM] running MicroVM
17
+ # @return [Object] block result
9
18
  def session(image_arn:, role_arn:, after: :suspend, client: Client.new, **run_options)
10
19
  vm = client.image(image_arn).run(role_arn: role_arn, **run_options)
11
20
  vm.wait_until_running
@@ -14,6 +23,12 @@ module Lambda
14
23
  cleanup(vm, after) if vm
15
24
  end
16
25
 
26
+ # Apply a session cleanup policy to a MicroVM.
27
+ #
28
+ # @param vm [MicroVM] MicroVM to clean up
29
+ # @param after [Symbol, nil] cleanup policy
30
+ # @return [Object, nil] cleanup result
31
+ # @raise [ArgumentError] when the policy is unknown
17
32
  def cleanup(vm, after) # rubocop:disable Naming/MethodParameterName
18
33
  case after
19
34
  when :keep, nil
@@ -6,6 +6,11 @@ module Lambda
6
6
  module Util
7
7
  module_function
8
8
 
9
+ # Extract the first non-nil value from an object method or hash-like key.
10
+ #
11
+ # @param value [Object] response object, hash, or SDK structure
12
+ # @param keys [Array<Symbol,String>] candidate method or key names
13
+ # @return [Object, nil] the first extracted non-nil value
9
14
  def extract(value, *keys)
10
15
  keys.each do |key|
11
16
  if value.respond_to?(key)
@@ -26,6 +31,10 @@ module Lambda
26
31
  nil
27
32
  end
28
33
 
34
+ # Normalize a provider state value into a lowercase symbol.
35
+ #
36
+ # @param value [Object] state-like value
37
+ # @return [Symbol] normalized state
29
38
  def normalize_state(value)
30
39
  value.to_s.downcase.to_sym
31
40
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Lambda
4
4
  module MicroVMs
5
- VERSION = '0.0.0'
5
+ # Current gem version.
6
+ VERSION = '0.1.0'
6
7
  end
7
8
  end
@@ -4,7 +4,9 @@ module Lambda
4
4
  module MicroVMs
5
5
  # Polls a resource until a desired lifecycle state is reached.
6
6
  class Waiter
7
+ # Default polling delay, in seconds.
7
8
  DEFAULT_DELAY = 1.0
9
+ # Default maximum wait time, in seconds.
8
10
  DEFAULT_TIMEOUT = 60.0
9
11
 
10
12
  def initialize(delay: DEFAULT_DELAY, timeout: DEFAULT_TIMEOUT, sleeper: Kernel)
@@ -13,6 +15,12 @@ module Lambda
13
15
  @sleeper = sleeper
14
16
  end
15
17
 
18
+ # Poll until the block returns a truthy value or the timeout expires.
19
+ #
20
+ # @param message [String] human-readable condition for timeout errors
21
+ # @yieldreturn [Object, false, nil] truthy value when the condition is satisfied
22
+ # @return [Object] the truthy block result
23
+ # @raise [WaitTimeoutError] if the timeout expires
16
24
  def wait(message: 'condition')
17
25
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
18
26
 
@@ -15,11 +15,16 @@ require_relative 'microvms/packager'
15
15
  require_relative 'microvms/deployer'
16
16
  require_relative 'microvms/doctor'
17
17
 
18
+ # Namespace for Lambda-related libraries.
18
19
  module Lambda
19
20
  # Idiomatic Ruby lifecycle helpers for AWS Lambda MicroVMs.
20
21
  module MicroVMs
21
22
  module_function
22
23
 
24
+ # Run a MicroVM from an image, yield it, and apply the requested cleanup policy.
25
+ #
26
+ # @see Lambda::MicroVMs::Session.session
27
+ # @return [Object] the block result
23
28
  def session(...)
24
29
  Session.session(...)
25
30
  end
@@ -3,6 +3,7 @@ module Lambda
3
3
  VERSION: String
4
4
 
5
5
  def self.session: (image_arn: String, role_arn: String, ?after: Symbol, ?client: Client, **untyped) { (MicroVM) -> untyped } -> untyped
6
+ def session: (image_arn: String, role_arn: String, ?after: Symbol, ?client: Client, **untyped) { (MicroVM) -> untyped } -> untyped
6
7
 
7
8
  class Error < StandardError
8
9
  end
@@ -26,7 +27,10 @@ module Lambda
26
27
  end
27
28
 
28
29
  class Client
30
+ REQUIRED_OPERATIONS: Array[Symbol]
29
31
  attr_reader sdk: untyped
32
+ def self.unsupported_operations: (?untyped sdk) -> Array[Symbol]
33
+ def self.sdk_contract_supported?: (?untyped sdk) -> bool
30
34
  def initialize: (?region: String?, ?profile: String?, ?sdk: untyped, **untyped) -> void
31
35
  def image: (String arn) -> Image
32
36
  def microvm: (String id_or_arn) -> MicroVM
@@ -43,6 +47,31 @@ module Lambda
43
47
  def create_auth_token: (**untyped) -> untyped
44
48
  def create_microvm_auth_token: (**untyped) -> untyped
45
49
  def call_sdk: (Symbol operation, **untyped) -> untyped
50
+
51
+ private
52
+
53
+ def build_sdk: (region: String?, profile: String?, **untyped) -> untyped
54
+ end
55
+
56
+ module Util
57
+ def extract: (untyped value, *untyped keys) -> untyped
58
+ def normalize_state: (untyped value) -> Symbol
59
+ def self.extract: (untyped value, *untyped keys) -> untyped
60
+ def self.normalize_state: (untyped value) -> Symbol
61
+ end
62
+
63
+ class Waiter
64
+ DEFAULT_DELAY: Float
65
+ DEFAULT_TIMEOUT: Float
66
+ def initialize: (?delay: Numeric, ?timeout: Numeric, ?sleeper: untyped) -> void
67
+ def wait: (?message: String) { () -> untyped } -> untyped
68
+ end
69
+
70
+ module Session
71
+ def session: (image_arn: String, role_arn: String, ?after: Symbol?, ?client: Client, **untyped) { (MicroVM) -> untyped } -> untyped
72
+ def cleanup: (MicroVM vm, Symbol? after) -> untyped
73
+ def self.session: (image_arn: String, role_arn: String, ?after: Symbol?, ?client: Client, **untyped) { (MicroVM) -> untyped } -> untyped
74
+ def self.cleanup: (MicroVM vm, Symbol? after) -> untyped
46
75
  end
47
76
 
48
77
  class Image
@@ -88,7 +117,13 @@ module Lambda
88
117
  def initialize: (url: String, token: String?, ?http: untyped) -> void
89
118
  def get: (String path, ?headers: Hash[String, String]) -> untyped
90
119
  def post: (String path, ?json: untyped, ?body: String?, ?headers: Hash[String, String]) -> untyped
91
- def request: (singleton(Net::HTTPGenericRequest) klass, String path, ?json: untyped, ?body: String?, ?headers: Hash[String, String]) -> untyped
120
+ def request: (untyped klass, String path, ?json: untyped, ?body: String?, ?headers: Hash[String, untyped]) -> untyped
121
+
122
+ private
123
+
124
+ def build_uri: (String? path) -> untyped
125
+ def parse_response: (untyped response) -> untyped
126
+ def build_request: (untyped klass, String path, ?headers: Hash[String, untyped], ?json: untyped, ?body: String?) -> untyped
92
127
  end
93
128
 
94
129
  class Project
@@ -113,7 +148,16 @@ module Lambda
113
148
  def payload: () -> Hash[String, untyped]
114
149
  def create_image_params: (artifact_uri: String) -> Hash[Symbol, untyped]
115
150
  def run_params: () -> Hash[Symbol, untyped]
151
+ def validate!: () -> Project
116
152
  def require!: (String field, untyped value) -> untyped
153
+
154
+ private
155
+
156
+ def validate_hash!: () -> void
157
+ def validate_lifecycle_after!: () -> void
158
+ def hash_config: (String path) -> Hash[untyped, untyped]
159
+ def fetch: (*String paths, default: untyped) -> untyped
160
+ def stringify_keys: (untyped value) -> untyped
117
161
  end
118
162
 
119
163
  class Scaffold
@@ -121,12 +165,23 @@ module Lambda
121
165
  attr_reader directory: String
122
166
  def initialize: (String name, ?directory: String, ?force: bool) -> void
123
167
  def create: () -> String
168
+
169
+ private
170
+
171
+ def ensure_target!: () -> void
172
+ def write: (String path, String content) -> untyped
173
+ def gemfile: () -> String
174
+ def dockerfile: () -> String
175
+ def app_rb: () -> String
176
+ def microvm_yml: () -> String
177
+ def readme: () -> String
178
+ def env_example: () -> String
124
179
  end
125
180
 
126
181
  class Packager
127
182
  DEFAULT_EXCLUDES: Array[String]
128
183
  attr_reader project: Project
129
- def initialize: (Project project) -> void
184
+ def initialize: (Project project, ?runner: untyped) -> void
130
185
  def package: (?output: String) -> String
131
186
  end
132
187
 
@@ -139,6 +194,10 @@ module Lambda
139
194
  def upload: (String path) -> String
140
195
  def deploy: () -> Image
141
196
  def run: () -> MicroVM
197
+
198
+ private
199
+
200
+ def build_s3: () -> untyped
142
201
  end
143
202
 
144
203
  class Doctor
@@ -148,6 +207,63 @@ module Lambda
148
207
  def initialize: (project: Project, ?runner: untyped) -> void
149
208
  def checks: () -> Array[untyped]
150
209
  def ok?: () -> bool
210
+
211
+ private
212
+
213
+ def check: (String name, untyped ok, untyped detail) -> untyped
214
+ def command_check: (String name, String command) -> untyped
215
+ def file_check: (String name, String path) -> untyped
216
+ def config_check: (String name, untyped value) -> untyped
217
+ def ric_check: () -> untyped
218
+ end
219
+ end
220
+ end
221
+
222
+ module Aws
223
+ module Lambda
224
+ class Client
225
+ def initialize: (**untyped) -> void
226
+ end
227
+ end
228
+
229
+ module S3
230
+ class Client
231
+ def initialize: (**untyped) -> void
151
232
  end
152
233
  end
153
234
  end
235
+
236
+ module FileUtils
237
+ def self.mkdir_p: (String | Array[String]) -> untyped
238
+ end
239
+
240
+ module JSON
241
+ def self.parse: (String source) -> untyped
242
+ def self.generate: (untyped value) -> String
243
+ end
244
+
245
+ module Net
246
+ class HTTP
247
+ def self.start: (String address, Integer port, ?use_ssl: bool) { (untyped) -> untyped } -> untyped
248
+
249
+ class Get
250
+ end
251
+
252
+ class Post
253
+ end
254
+ end
255
+ end
256
+
257
+ module Open3
258
+ def self.capture3: (*String command, ?chdir: String) -> [String, String, untyped]
259
+ end
260
+
261
+ module URI
262
+ def self.parse: (String uri) -> untyped
263
+ end
264
+
265
+ module YAML
266
+ def self.safe_load_file: (String filename, ?permitted_classes: Array[singleton(Symbol)], ?aliases: bool) -> untyped
267
+ end
268
+
269
+ $CHILD_STATUS: untyped
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lambda-microvms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenneth C. Demanawa
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-06-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: aws-sdk-lambda
@@ -91,7 +90,6 @@ metadata:
91
90
  source_code_uri: https://github.com/kanutocd/lambda-microvms
92
91
  changelog_uri: https://github.com/kanutocd/lambda-microvms/blob/main/CHANGELOG.md
93
92
  rubygems_mfa_required: 'true'
94
- post_install_message:
95
93
  rdoc_options: []
96
94
  require_paths:
97
95
  - lib
@@ -106,8 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
104
  - !ruby/object:Gem::Version
107
105
  version: '0'
108
106
  requirements: []
109
- rubygems_version: 3.4.19
110
- signing_key:
107
+ rubygems_version: 3.6.9
111
108
  specification_version: 4
112
109
  summary: Idiomatic Ruby lifecycle client for AWS Lambda MicroVMs.
113
110
  test_files: []