lambda-microvms 0.0.0 → 0.2.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.
@@ -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.2.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
 
@@ -5,7 +5,9 @@ require_relative 'microvms/error'
5
5
  require_relative 'microvms/util'
6
6
  require_relative 'microvms/waiter'
7
7
  require_relative 'microvms/endpoint'
8
+ require_relative 'microvms/adapters/microvm_sdk'
8
9
  require_relative 'microvms/client'
10
+ require_relative 'microvms/function_client'
9
11
  require_relative 'microvms/image'
10
12
  require_relative 'microvms/microvm'
11
13
  require_relative 'microvms/session'
@@ -15,11 +17,16 @@ require_relative 'microvms/packager'
15
17
  require_relative 'microvms/deployer'
16
18
  require_relative 'microvms/doctor'
17
19
 
20
+ # Namespace for Lambda-related libraries.
18
21
  module Lambda
19
22
  # Idiomatic Ruby lifecycle helpers for AWS Lambda MicroVMs.
20
23
  module MicroVMs
21
24
  module_function
22
25
 
26
+ # Run a MicroVM from an image, yield it, and apply the requested cleanup policy.
27
+ #
28
+ # @see Lambda::MicroVMs::Session.session
29
+ # @return [Object] the block result
23
30
  def session(...)
24
31
  Session.session(...)
25
32
  end