Class: Boxd::CLIBackend

Inherits:
Object
  • Object
show all
Defined in:
lib/boxd/cli_backend.rb

Overview

CLIBackend shells out to the ‘boxd` CLI for every operation. v0 of this gem is built on this; v1 will speak gRPC directly. The public API does not change between the two — only this file does.

Constant Summary collapse

DEFAULT_BIN =
"boxd"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil, bin: nil, environment: nil) ⇒ CLIBackend

Returns a new instance of CLIBackend.



17
18
19
20
21
22
23
24
# File 'lib/boxd/cli_backend.rb', line 17

def initialize(api_key: nil, bin: nil, environment: nil)
  @bin = bin || ENV["BOXD_BIN"] || DEFAULT_BIN
  @env = {}
  @env["BOXD_TOKEN"]       = api_key     if api_key
  @env["BOXD_ENVIRONMENT"] = environment if environment

  assert_cli_present!
end

Instance Attribute Details

#binObject (readonly)

Returns the value of attribute bin.



15
16
17
# File 'lib/boxd/cli_backend.rb', line 15

def bin
  @bin
end

#envObject (readonly)

Returns the value of attribute env.



15
16
17
# File 'lib/boxd/cli_backend.rb', line 15

def env
  @env
end

Instance Method Details

#call_json(*args) ⇒ Object

Run ‘boxd <args>` and parse `–json` output. Raises on non-zero exit with a typed Boxd::Error subclass when we recognise the failure.



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/boxd/cli_backend.rb', line 28

def call_json(*args)
  out, err, status = Open3.capture3(env, bin, "--json", *args.map(&:to_s))
  unless status.success?
    raise classify_error(err.empty? ? out : err, args)
  end

  return nil if out.strip.empty?

  JSON.parse(out, symbolize_names: true)
rescue JSON::ParserError => e
  raise InternalError, "boxd CLI returned non-JSON output for `#{args.join(' ')}`: #{e.message}\n#{out}"
end

#call_raw(*args) ⇒ Object

Run ‘boxd <args>` and return [stdout, stderr] as strings. Used for commands without –json support.



43
44
45
46
47
48
# File 'lib/boxd/cli_backend.rb', line 43

def call_raw(*args)
  out, err, status = Open3.capture3(env, bin, *args.map(&:to_s))
  raise classify_error(err.empty? ? out : err, args) unless status.success?

  [out, err]
end

#exec_stream(vm, cmd, env: nil, tty: false, &block) ⇒ Object

Stream ‘boxd exec` for a single command, yielding chunks. Returns the exit code captured from CLI exit status.

The CLI does not stream stdout/stderr separately by default; we capture both via pipes and yield each line as it arrives, tagging the stream.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/boxd/cli_backend.rb', line 56

def exec_stream(vm, cmd, env: nil, tty: false, &block)
  cli_args = ["exec"]
  cli_args << "--tty" if tty
  Array(env).each do |k, v|
    cli_args.push("-e", "#{k}=#{v}")
  end
  cli_args << vm.to_s
  cli_args << "--"
  cli_args.concat(Array(cmd).map(&:to_s))

  stdout_buf = +""
  stderr_buf = +""
  exit_code  = nil

  Open3.popen3(self.env, bin, *cli_args) do |_stdin, stdout, stderr, wait_thr|
    threads = []
    threads << Thread.new do
      stdout.each_line do |line|
        stdout_buf << line
        block&.call(:stdout, line)
      end
    end
    threads << Thread.new do
      stderr.each_line do |line|
        stderr_buf << line
        block&.call(:stderr, line)
      end
    end
    threads.each(&:join)
    exit_code = wait_thr.value.exitstatus
  end

  { stdout: stdout_buf, stderr: stderr_buf, exit_code: exit_code }
end