beaker-kubevirt 1.0.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.
@@ -0,0 +1,593 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubeclient'
4
+ require 'yaml'
5
+ require 'tempfile'
6
+ require 'base64'
7
+
8
+ module Beaker
9
+ # Helper class for KubeVirt operations
10
+ class KubevirtHelper
11
+ attr_reader :namespace, :options, :k8s_client, :kubevirt_client, :cdi_client
12
+
13
+ def initialize(options)
14
+ @options = options
15
+ @namespace = options[:namespace] || 'default'
16
+ raw_kubeconfig = options[:kubeconfig] || ENV['KUBECONFIG'] || File.join(Dir.home, '.kube', 'config')
17
+ @kubeconfig_path = File.expand_path(raw_kubeconfig)
18
+ @kubecontext = options[:kubecontext] || ENV.fetch('KUBECONTEXT', nil)
19
+ @logger = options[:logger]
20
+
21
+ # Store original SSL options for port forwarding (before kubeclient processes them)
22
+ @original_ssl_options = nil
23
+
24
+ # Keep temp files alive to prevent garbage collection
25
+ @temp_files = []
26
+
27
+ # Allow injection of clients for testing
28
+ @k8s_client = options[:k8s_client]
29
+ @kubevirt_client = options[:kubevirt_client]
30
+ @cdi_client = options[:cdi_client]
31
+
32
+ # Only setup clients if not provided (for testing)
33
+ return if @k8s_client && @kubevirt_client
34
+
35
+ # Manually extract SSL options with CA file by parsing kubeconfig directly
36
+ # (Kubeclient context doesn't expose the raw CA file path/data)
37
+ @original_ssl_options = extract_ssl_from_kubeconfig
38
+ setup_kubernetes_client
39
+ setup_kubevirt_client
40
+ setup_cdi_client unless @cdi_client
41
+ end
42
+
43
+ ##
44
+ # Create a virtual machine
45
+ # @param [Hash] vm_spec The VM specification
46
+ def create_vm(vm_spec)
47
+ # Convert all keys to symbols recursively
48
+ vm_spec_sym = symbolize_keys(vm_spec)
49
+ @kubevirt_client.create_virtual_machine(vm_spec_sym)
50
+ end
51
+
52
+ ##
53
+ # Create a secret
54
+ # @param [Hash] secret_spec The secret specification
55
+ def create_secret(secret_spec)
56
+ # Convert all keys to symbols recursively
57
+ secret_spec_sym = symbolize_keys(secret_spec)
58
+ @k8s_client.create_secret(secret_spec_sym)
59
+ end
60
+
61
+ ##
62
+ # Get a virtual machine
63
+ # @param [String] vm_name The VM name
64
+ # @return [Hash] VM object
65
+ def get_vm(vm_name)
66
+ @kubevirt_client.get_virtual_machine(vm_name, @namespace)
67
+ rescue Kubeclient::ResourceNotFoundError
68
+ nil
69
+ end
70
+
71
+ ##
72
+ # Get a virtual machine instance
73
+ # @param [String] vmi_name The VMI name
74
+ # @return [Hash] VMI object
75
+ def get_vmi(vmi_name)
76
+ @kubevirt_client.get_virtual_machine_instance(vmi_name, @namespace)
77
+ rescue Kubeclient::ResourceNotFoundError
78
+ nil
79
+ end
80
+
81
+ ##
82
+ # Get the virt-launcher pod backing a VMI.
83
+ # @param [String] vmi_name The VMI name (== VM name)
84
+ # @return [Hash, nil] pod object, or nil if not yet scheduled
85
+ def get_virt_launcher_pod(vmi_name)
86
+ pods = @k8s_client.get_pods(namespace: @namespace,
87
+ label_selector: "kubevirt.io/vm=#{vmi_name}")
88
+ pods.first
89
+ rescue Kubeclient::ResourceNotFoundError
90
+ nil
91
+ end
92
+
93
+ ##
94
+ # Delete a virtual machine
95
+ # @param [String] vm_name The VM name
96
+ def delete_vm(vm_name)
97
+ @kubevirt_client.delete_virtual_machine(vm_name, @namespace)
98
+ @logger.debug("Deleted VM #{vm_name}")
99
+ rescue Kubeclient::ResourceNotFoundError
100
+ @logger.debug("VM #{vm_name} not found during deletion")
101
+ end
102
+
103
+ ##
104
+ # Cleanup VMs created by this test group
105
+ # @param [String] test_group_identifier The identifier for the test group
106
+ def cleanup_vms(test_group_identifier)
107
+ @logger.info("Cleaning up VMs for test group: #{test_group_identifier}")
108
+ label_selector = "beaker/test-group=#{test_group_identifier}"
109
+ @logger.debug("Using label selector: #{label_selector}")
110
+
111
+ vms = @kubevirt_client.get_virtual_machines(namespace: @namespace,
112
+ label_selector: label_selector)
113
+ @logger.info("Found #{vms.length} VM(s) with label beaker/test-group=#{test_group_identifier}")
114
+
115
+ vms.each do |vm|
116
+ # Extract VM name - access hash-style to avoid RSpec metadata collision
117
+ vm_metadata = vm[:metadata] || vm['metadata']
118
+ vm_name = vm_metadata[:name] || vm_metadata['name'] if vm_metadata
119
+
120
+ unless vm_name && !vm_name.empty?
121
+ @logger.error('Cannot delete VM with empty or nil name.')
122
+ @logger.error("VM structure: #{vm.inspect[0..1000]}")
123
+ next
124
+ end
125
+
126
+ vm_labels = vm_metadata[:labels] || vm_metadata['labels'] if vm_metadata
127
+ @logger.debug("Deleting VM #{vm_name} with labels: #{vm_labels.inspect}")
128
+ @kubevirt_client.delete_virtual_machine(vm_name, @namespace)
129
+ @logger.info("Deleted VM #{vm_name}")
130
+ rescue Kubeclient::ResourceNotFoundError
131
+ @logger.debug("VM #{vm_name} not found during cleanup")
132
+ rescue StandardError => e
133
+ @logger.error("Error deleting VM #{vm_name}: #{e.message}")
134
+ @logger.error("Backtrace: #{e.backtrace[0..5].join("\n")}")
135
+ end
136
+ end
137
+
138
+ ##
139
+ # Cleanup secrets associated with a test group
140
+ # @param [String] test_group_identifier The identifier for the test group
141
+ def cleanup_secrets(test_group_identifier)
142
+ @logger.info("Cleaning up secrets for test group: #{test_group_identifier}")
143
+ label_selector = "beaker/test-group=#{test_group_identifier}"
144
+ @logger.debug("Using label selector: #{label_selector}")
145
+
146
+ secrets = @k8s_client.get_secrets(namespace: @namespace,
147
+ label_selector: label_selector)
148
+ @logger.info("Found #{secrets.length} secret(s) with label beaker/test-group=#{test_group_identifier}")
149
+
150
+ secrets.each do |secret|
151
+ # Extract secret name - access hash-style to avoid RSpec metadata collision
152
+ secret_metadata = secret[:metadata] || secret['metadata']
153
+ secret_name = secret_metadata[:name] || secret_metadata['name'] if secret_metadata
154
+
155
+ unless secret_name && !secret_name.empty?
156
+ @logger.error('Cannot delete secret with empty or nil name.')
157
+ @logger.error("Secret structure: #{secret.inspect[0..1000]}")
158
+ next
159
+ end
160
+
161
+ secret_labels = secret_metadata[:labels] || secret_metadata['labels'] if secret_metadata
162
+ @logger.debug("Deleting secret #{secret_name} with labels: #{secret_labels.inspect}")
163
+ @k8s_client.delete_secret(secret_name, @namespace)
164
+ @logger.info("Deleted secret #{secret_name}")
165
+ rescue Kubeclient::ResourceNotFoundError
166
+ @logger.debug("Secret #{secret_name} not found during cleanup")
167
+ rescue StandardError => e
168
+ @logger.error("Error deleting secret #{secret_name}: #{e.message}")
169
+ end
170
+ end
171
+
172
+ ##
173
+ # Cleanup services associated with a test group
174
+ # @param [String] test_group_identifier The identifier for the test group
175
+ def cleanup_services(test_group_identifier)
176
+ @logger.info("Cleaning up services for test group: #{test_group_identifier}")
177
+ label_selector = "beaker/test-group=#{test_group_identifier}"
178
+ @logger.debug("Using label selector: #{label_selector}")
179
+
180
+ services = @k8s_client.get_services(namespace: @namespace,
181
+ label_selector: label_selector)
182
+ @logger.info("Found #{services.length} service(s) with label beaker/test-group=#{test_group_identifier}")
183
+
184
+ services.each do |service|
185
+ # Extract service name - access hash-style to avoid RSpec metadata collision
186
+ service_metadata = service[:metadata] || service['metadata']
187
+ service_name = service_metadata[:name] || service_metadata['name'] if service_metadata
188
+
189
+ unless service_name && !service_name.empty?
190
+ @logger.error('Cannot delete service with empty or nil name.')
191
+ @logger.error("Service structure: #{service.inspect[0..1000]}")
192
+ next
193
+ end
194
+
195
+ service_labels = service_metadata[:labels] || service_metadata['labels'] if service_metadata
196
+ @logger.debug("Deleting service #{service_name} with labels: #{service_labels.inspect}")
197
+ @k8s_client.delete_service(service_name, @namespace)
198
+ @logger.info("Deleted service #{service_name}")
199
+ rescue Kubeclient::ResourceNotFoundError
200
+ @logger.debug("Service #{service_name} not found during cleanup")
201
+ rescue StandardError => e
202
+ @logger.error("Error deleting service #{service_name}: #{e.message}")
203
+ end
204
+ end
205
+
206
+ ##
207
+ # Cleanup DataVolumes associated with a test group. No-op if CDI isn't
208
+ # available on the cluster (the @cdi_client stays nil in that case).
209
+ # @param [String] test_group_identifier The identifier for the test group
210
+ def cleanup_data_volumes(test_group_identifier)
211
+ return unless @cdi_client
212
+
213
+ @logger.info("Cleaning up DataVolumes for test group: #{test_group_identifier}")
214
+ label_selector = "beaker/test-group=#{test_group_identifier}"
215
+
216
+ begin
217
+ dvs = @cdi_client.get_data_volumes(namespace: @namespace, label_selector: label_selector)
218
+ rescue StandardError => e
219
+ @logger.warn("Failed to list DataVolumes for cleanup: #{e.class}: #{e.message}")
220
+ return
221
+ end
222
+
223
+ @logger.info("Found #{dvs.length} DataVolume(s) with label #{label_selector}")
224
+
225
+ dvs.each do |dv|
226
+ md = dv[:metadata] || dv['metadata']
227
+ dv_name = md[:name] || md['name'] if md
228
+ unless dv_name && !dv_name.empty?
229
+ @logger.error("Skipping DataVolume with missing name: #{dv.inspect[0, 200]}")
230
+ next
231
+ end
232
+
233
+ @cdi_client.delete_data_volume(dv_name, @namespace)
234
+ @logger.info("Deleted DataVolume #{dv_name}")
235
+ rescue Kubeclient::ResourceNotFoundError
236
+ @logger.debug("DataVolume #{dv_name} not found during cleanup")
237
+ rescue StandardError => e
238
+ @logger.error("Error deleting DataVolume #{dv_name}: #{e.message}")
239
+ end
240
+ end
241
+
242
+ ##
243
+ # Setup port forwarding for a VM
244
+ # @param [String] vm_name The VM name
245
+ # @param [Integer] vm_port The VM port to forward
246
+ # @param [Integer] local_port The local port to forward to
247
+ # @return [Process] The port-forward process
248
+ def setup_port_forward(vm_name, vm_port, local_port)
249
+ require 'beaker/hypervisor/port_forward'
250
+ forwarder = KubeVirtPortForwarder.new(
251
+ kube_client: @kubevirt_client,
252
+ namespace: @namespace,
253
+ vmi_name: vm_name,
254
+ target_port: vm_port,
255
+ local_port: local_port,
256
+ logger: @logger,
257
+ on_error: method(:forwarder_error_handler),
258
+ ssl_options: @original_ssl_options,
259
+ )
260
+
261
+ # Start the port forwarder in a background thread
262
+ forwarder.start
263
+
264
+ # Check if the forwarder started correctly.
265
+ return forwarder if forwarder.state == :running
266
+
267
+ @logger.error("Port forwarder failed to start for VM #{vm_name} on port #{vm_port}")
268
+ raise "Port forwarder failed to start for VM #{vm_name} on port #{vm_port}"
269
+ end
270
+
271
+ def forwarder_error_handler(error)
272
+ @logger.error("Port forwarder error: #{error.message}")
273
+ # Optionally, you can implement retry logic or cleanup here
274
+ end
275
+
276
+ ##
277
+ # Create a NodePort service for SSH access
278
+ # @param [String] vm_name The VM name
279
+ # @param [String] service_name The service name
280
+ # @param [String, nil] test_group_identifier Test-group identifier applied as
281
+ # the +beaker/test-group+ label so +cleanup_services+ can match the
282
+ # service. When +nil+, only the +beaker/vm+ label is set (legacy behavior).
283
+ # @return [Hash] Service object
284
+ def create_nodeport_service(vm_name, service_name, test_group_identifier = nil)
285
+ labels = { 'beaker/vm' => vm_name }
286
+ labels['beaker/test-group'] = test_group_identifier if test_group_identifier
287
+
288
+ service_spec = {
289
+ 'apiVersion' => 'v1',
290
+ 'kind' => 'Service',
291
+ 'metadata' => {
292
+ 'name' => service_name,
293
+ 'namespace' => @namespace,
294
+ 'labels' => labels,
295
+ },
296
+ 'spec' => {
297
+ 'type' => 'NodePort',
298
+ 'selector' => {
299
+ 'kubevirt.io/vm' => vm_name,
300
+ },
301
+ 'ports' => [
302
+ {
303
+ 'name' => 'ssh',
304
+ 'port' => 22,
305
+ 'targetPort' => 22,
306
+ 'protocol' => 'TCP',
307
+ },
308
+ ],
309
+ },
310
+ }
311
+
312
+ service_spec_sym = symbolize_keys(service_spec)
313
+ @k8s_client.create_service(service_spec_sym)
314
+ end
315
+
316
+ ##
317
+ # Unlink the kubeconfig-derived cert/key tempfiles kept alive for the
318
+ # process lifetime. Safe to call after all API operations that need the
319
+ # files have completed.
320
+ def cleanup_temp_files
321
+ return unless @temp_files
322
+
323
+ @temp_files.each do |file|
324
+ path = file.path
325
+ file.close unless file.closed?
326
+ file.unlink
327
+ rescue StandardError => e
328
+ @logger&.warn("Failed to unlink temp file #{path}: #{e.message}")
329
+ end
330
+ @temp_files.clear
331
+ end
332
+
333
+ ##
334
+ # Get a cluster node IP address
335
+ # @return [String] Node IP address
336
+ def node_ip
337
+ nodes = @k8s_client.get_nodes
338
+ node = nodes.first
339
+
340
+ # Try to get external IP first, fallback to internal IP
341
+ addresses = node.dig('status', 'addresses') || []
342
+ external_ip = addresses.find { |addr| addr['type'] == 'ExternalIP' }
343
+ internal_ip = addresses.find { |addr| addr['type'] == 'InternalIP' }
344
+
345
+ if external_ip
346
+ external_ip['address']
347
+ elsif internal_ip
348
+ internal_ip['address']
349
+ else
350
+ raise 'Could not determine node IP address'
351
+ end
352
+ end
353
+
354
+ private
355
+
356
+ ##
357
+ # Setup Kubernetes API client
358
+ def setup_kubernetes_client
359
+ config = Kubeclient::Config.new(load_kubeconfig, File.dirname(@kubeconfig_path))
360
+ context_config = config.context(@kubecontext)
361
+
362
+ @k8s_client = Kubeclient::Client.new(
363
+ context_config.api_endpoint,
364
+ context_config.api_version,
365
+ ssl_options: context_config.ssl_options,
366
+ auth_options: context_config.auth_options,
367
+ )
368
+ rescue StandardError => e
369
+ # For testing or when Kubeclient can't parse, fall back to manual parsing
370
+ @logger&.warn("Failed to use Kubeclient::Config, falling back to manual parsing: #{e.message}")
371
+ setup_kubernetes_client_manual
372
+ end
373
+
374
+ ##
375
+ # Setup KubeVirt API client
376
+ def setup_kubevirt_client
377
+ config = Kubeclient::Config.new(load_kubeconfig, File.dirname(@kubeconfig_path))
378
+ context_config = config.context(@kubecontext)
379
+
380
+ @kubevirt_client = Kubeclient::Client.new(
381
+ "#{context_config.api_endpoint}/apis/kubevirt.io",
382
+ context_config.api_version,
383
+ ssl_options: context_config.ssl_options,
384
+ auth_options: context_config.auth_options,
385
+ )
386
+ rescue StandardError => e
387
+ # For testing or when Kubeclient can't parse, fall back to manual parsing
388
+ @logger&.warn("Failed to use Kubeclient::Config, falling back to manual parsing: #{e.message}")
389
+ setup_kubevirt_client_manual
390
+ end
391
+
392
+ ##
393
+ # Setup CDI (Containerized Data Importer) API client used for managing
394
+ # DataVolumes. Best-effort: clusters without CDI installed will still be
395
+ # able to provision container-disk / direct-PVC VMs. We probe the API
396
+ # group with a `discover` call so that clusters lacking CDI leave
397
+ # @cdi_client nil rather than appearing live and warning on every
398
+ # cleanup.
399
+ def setup_cdi_client
400
+ config = Kubeclient::Config.new(load_kubeconfig, File.dirname(@kubeconfig_path))
401
+ context_config = config.context(@kubecontext)
402
+ client = Kubeclient::Client.new(
403
+ "#{context_config.api_endpoint}/apis/cdi.kubevirt.io",
404
+ 'v1beta1',
405
+ ssl_options: context_config.ssl_options,
406
+ auth_options: context_config.auth_options,
407
+ )
408
+ client.discover
409
+ @cdi_client = client
410
+ rescue StandardError => e
411
+ @logger&.debug("CDI client setup skipped: #{e.class}: #{e.message}")
412
+ @cdi_client = nil
413
+ end
414
+
415
+ ##
416
+ # Setup Kubernetes API client using manual kubeconfig parsing
417
+ def setup_kubernetes_client_manual
418
+ config = load_kubeconfig
419
+ context_config = get_context_config(config)
420
+ ssl_options = setup_ssl_options(context_config)
421
+ auth_options = setup_auth_options(context_config)
422
+
423
+ # Store original SSL options before kubeclient processes them
424
+ @original_ssl_options = ssl_options.dup
425
+
426
+ @k8s_client = Kubeclient::Client.new(
427
+ context_config['cluster']['server'],
428
+ 'v1',
429
+ ssl_options: ssl_options,
430
+ auth_options: auth_options,
431
+ )
432
+ end
433
+
434
+ ##
435
+ # Setup KubeVirt API client using manual kubeconfig parsing
436
+ def setup_kubevirt_client_manual
437
+ config = load_kubeconfig
438
+ context_config = get_context_config(config)
439
+ ssl_options = setup_ssl_options(context_config)
440
+ auth_options = setup_auth_options(context_config)
441
+
442
+ # Store original SSL options before kubeclient processes them
443
+ @original_ssl_options = ssl_options.dup
444
+
445
+ kubevirt_endpoint = "#{context_config['cluster']['server']}/apis/kubevirt.io"
446
+ @kubevirt_client = Kubeclient::Client.new(
447
+ kubevirt_endpoint,
448
+ 'v1',
449
+ ssl_options: ssl_options,
450
+ auth_options: auth_options,
451
+ )
452
+ end
453
+
454
+ ##
455
+ # Load kubeconfig file
456
+ # @return [Hash] Parsed kubeconfig
457
+ def load_kubeconfig
458
+ return @kubeconfig_data if @kubeconfig_data
459
+ raise "Kubeconfig file not found: #{@kubeconfig_path}" unless File.exist?(@kubeconfig_path)
460
+ raise "Kubeconfig path is not a regular file: #{@kubeconfig_path}" unless File.file?(@kubeconfig_path)
461
+
462
+ @logger&.warn("kubeconfig #{@kubeconfig_path} is a symlink to #{File.realpath(@kubeconfig_path)}") if File.symlink?(@kubeconfig_path)
463
+
464
+ @kubeconfig_data = YAML.safe_load_file(@kubeconfig_path)
465
+ @kubeconfig_data
466
+ end
467
+
468
+ ##
469
+ # Get context configuration from kubeconfig
470
+ # @param [Hash] config The kubeconfig
471
+ # @return [Hash] Context configuration
472
+ def get_context_config(config)
473
+ current_context = @kubecontext || config['current-context']
474
+ raise 'No current context specified' unless current_context
475
+
476
+ context = config['contexts'].find { |ctx| ctx['name'] == current_context }
477
+ raise "Context '#{current_context}' not found" unless context
478
+
479
+ cluster_name = context.dig('context', 'cluster')
480
+ user_name = context.dig('context', 'user')
481
+
482
+ cluster = config['clusters'].find { |c| c['name'] == cluster_name }
483
+ user = config['users'].find { |u| u['name'] == user_name }
484
+
485
+ raise "Cluster '#{cluster_name}' not found" unless cluster
486
+ raise "User '#{user_name}' not found" unless user
487
+
488
+ {
489
+ 'cluster' => cluster['cluster'],
490
+ 'user' => user['user'],
491
+ 'namespace' => context.dig('context', 'namespace') || @namespace,
492
+ }
493
+ end
494
+
495
+ ##
496
+ # Write content to a temporary file
497
+ # @param [String] prefix File prefix
498
+ # @param [String] content File content
499
+ # @return [String] Path to temporary file
500
+ def write_temp_file(prefix, content)
501
+ file = Tempfile.new([prefix, '.pem'])
502
+ file.binmode
503
+ file.write(content)
504
+ file.flush
505
+ file.close
506
+ # Keep reference to prevent garbage collection and file deletion
507
+ @temp_files << file
508
+ @logger&.debug("Created temp file: #{file.path} (#{content.bytesize} bytes)")
509
+ file.path
510
+ end
511
+
512
+ ##
513
+ # Recursively convert hash keys to symbols
514
+ def symbolize_keys(obj)
515
+ case obj
516
+ when Hash
517
+ obj.each_with_object({}) do |(k, v), memo|
518
+ memo[k.to_sym] = symbolize_keys(v)
519
+ end
520
+ when Array
521
+ obj.map { |v| symbolize_keys(v) }
522
+ else
523
+ obj
524
+ end
525
+ end
526
+
527
+ ##
528
+ # Extract SSL options directly from kubeconfig file
529
+ # This preserves the :ca_file path before kubeclient converts it to :cert_store
530
+ # @return [Hash] SSL options with :ca_file preserved
531
+ def extract_ssl_from_kubeconfig
532
+ config = load_kubeconfig
533
+ context_config = get_context_config(config)
534
+ ssl_options = setup_ssl_options(context_config)
535
+ @logger&.debug("Extracted SSL options from kubeconfig: #{ssl_options.keys.join(', ')}")
536
+ ssl_options
537
+ end
538
+
539
+ ##
540
+ # Setup SSL options from context config
541
+ # @param [Hash] context_config The context configuration
542
+ # @return [Hash] SSL options for Kubeclient
543
+ def setup_ssl_options(context_config)
544
+ ssl_options = {}
545
+ cluster_config = context_config['cluster']
546
+
547
+ if cluster_config['certificate-authority-data']
548
+ ca_cert = Base64.strict_decode64(cluster_config['certificate-authority-data'])
549
+ ca_file_path = write_temp_file('ca-cert', ca_cert)
550
+ ssl_options[:ca_file] = ca_file_path
551
+ elsif cluster_config['certificate-authority']
552
+ ssl_options[:ca_file] = cluster_config['certificate-authority']
553
+ end
554
+
555
+ ssl_options[:verify_ssl] = false if cluster_config['insecure-skip-tls-verify']
556
+
557
+ ssl_options
558
+ end
559
+
560
+ ##
561
+ # Setup auth options from context config
562
+ # @param [Hash] context_config The context configuration
563
+ # @return [Hash] Auth options for Kubeclient
564
+ def setup_auth_options(context_config)
565
+ auth_options = {}
566
+ user_config = context_config['user']
567
+
568
+ if user_config['token']
569
+ auth_options[:bearer_token] = user_config['token']
570
+ elsif user_config['tokenFile']
571
+ token_file_path = user_config['tokenFile']
572
+ raise "Token file not found: #{token_file_path}" unless File.exist?(token_file_path)
573
+
574
+ auth_options[:bearer_token] = File.read(token_file_path).strip
575
+
576
+ elsif user_config['client-certificate-data'] && user_config['client-key-data']
577
+ client_cert = Base64.strict_decode64(user_config['client-certificate-data'])
578
+ client_key = Base64.strict_decode64(user_config['client-key-data'])
579
+
580
+ cert_file_path = write_temp_file('client-cert', client_cert)
581
+ key_file_path = write_temp_file('client-key', client_key)
582
+
583
+ auth_options[:client_cert] = cert_file_path
584
+ auth_options[:client_key] = key_file_path
585
+ elsif user_config['client-certificate'] && user_config['client-key']
586
+ auth_options[:client_cert] = user_config['client-certificate']
587
+ auth_options[:client_key] = user_config['client-key']
588
+ end
589
+
590
+ auth_options
591
+ end
592
+ end
593
+ end