├── .ruby-version ├── .rspec ├── lib ├── orka_api_client.rb └── orka_api_client │ ├── version.rb │ ├── models │ ├── password_requirements.rb │ ├── attr_predicate.rb │ ├── enumerator.rb │ ├── protocol_port_mapping.rb │ ├── disk.rb │ ├── token_info.rb │ ├── remote_image.rb │ ├── remote_iso.rb │ ├── lazy_model.rb │ ├── kube_account.rb │ ├── vm_deployment_result.rb │ ├── user.rb │ ├── iso.rb │ ├── image.rb │ ├── node.rb │ ├── vm_configuration.rb │ ├── vm_instance.rb │ └── vm_resource.rb │ ├── port_mapping.rb │ ├── errors.rb │ ├── connection.rb │ ├── auth_middleware.rb │ └── client.rb ├── spec ├── orka_api │ └── client_spec.rb └── spec_helper.rb ├── bin ├── setup └── console ├── .yardopts ├── .gitignore ├── .editorconfig ├── .parlour ├── Gemfile ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── main.yml │ ├── actionlint.yml │ └── stale-issues.yml ├── Rakefile ├── orka_api_client.gemspec ├── LICENSE ├── README.md ├── .rubocop.yml └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.7 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/orka_api_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "orka_api_client/version" 4 | require_relative "orka_api_client/client" 5 | -------------------------------------------------------------------------------- /spec/orka_api/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe OrkaAPI::Client do # rubocop:disable RSpec/EmptyExampleGroup 4 | # TODO 5 | end 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | lib/orka_api_client/models/attr_predicate.rb 3 | lib/orka_api_client/models/lazy_model.rb 4 | lib/orka_api_client/client.rb 5 | lib/**/*.rb 6 | - 7 | LICENSE 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /rbi/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.parlour: -------------------------------------------------------------------------------- 1 | parser: false 2 | 3 | requires: 4 | - sord 5 | 6 | excluded_modules: 7 | - OrkaAPI::AuthMiddleware 8 | 9 | plugins: 10 | Sord::ParlourPlugin: 11 | exclude_untyped: yes 12 | hide_private: yes 13 | rbi: yes 14 | tags: [] 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in orka_api_client.gemspec 6 | gemspec 7 | 8 | gem "parlour" 9 | gem "rake" 10 | gem "rspec" 11 | gem "rubocop" 12 | gem "rubocop-performance" 13 | gem "rubocop-rake" 14 | gem "rubocop-rspec" 15 | gem "sord", "~> 6.0" 16 | gem "yard" 17 | -------------------------------------------------------------------------------- /lib/orka_api_client/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | class Client 5 | # The version of this gem. 6 | VERSION = "0.2.1" 7 | 8 | # The Orka API version this gem supports. Support for other versions is not guaranteed, in particular older 9 | # versions. 10 | API_VERSION = "2.4.0" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "orka_api_client" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "orka_api_client" 4 | 5 | RSpec.configure do |config| 6 | # Enable flags like --only-failures and --next-failure 7 | config.example_status_persistence_file_path = ".rspec_status" 8 | 9 | # Disable RSpec exposing methods globally on `Module` and `main` 10 | config.disable_monkey_patching! 11 | 12 | config.expect_with :rspec do |c| 13 | c.syntax = :expect 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/password_requirements.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # The requirements enforced for passwords when creating a user account. 6 | class PasswordRequirements 7 | # @return [Integer] The minimum length of a password. 8 | attr_reader :length 9 | 10 | # @api private 11 | # @param [Hash] hash 12 | def initialize(hash) 13 | @length = hash["password_length"] 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | --- 3 | version: 2 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: "/" 7 | schedule: 8 | interval: weekly 9 | allow: 10 | - dependency-type: all 11 | ignore: 12 | - dependency-name: actions/stale 13 | groups: 14 | artifacts: 15 | patterns: 16 | - actions/*-artifact 17 | - package-ecosystem: bundler 18 | directory: "/" 19 | schedule: 20 | interval: daily 21 | allow: 22 | - dependency-type: all 23 | 24 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/attr_predicate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # @private 6 | module AttrPredicate 7 | private 8 | 9 | # @!parse 10 | # # @!macro [attach] attr_predicate 11 | # # @!attribute [r] $1? 12 | # def self.attr_predicate(*); end 13 | def attr_predicate(*attrs) 14 | attrs.each do |attr| 15 | define_method :"#{attr}?" do 16 | instance_variable_get(:"@#{attr}") 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | require "rubocop/rake_task" 8 | RuboCop::RakeTask.new 9 | 10 | require "yard" 11 | YARD::Rake::YardocTask.new 12 | 13 | task build: [:date_epoch, :parlour] 14 | 15 | desc "Set SOURCE_DATE_EPOCH" 16 | task :date_epoch do 17 | ENV["SOURCE_DATE_EPOCH"] = IO.popen(%W[git -C #{__dir__} log -1 --format=%ct], &:read).chomp 18 | end 19 | 20 | desc "Generate RBI" 21 | task :parlour do 22 | system("bundle exec parlour") || abort 23 | end 24 | 25 | task default: :build 26 | -------------------------------------------------------------------------------- /lib/orka_api_client/port_mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | # Represents a port forwarding from a host node to a guest VM. 5 | class PortMapping 6 | # @return [Integer] The port on the node side. 7 | attr_reader :host_port 8 | 9 | # @return [Integer] The port on the VM side. 10 | attr_reader :guest_port 11 | 12 | # @param [Integer] host_port The port on the node side. 13 | # @param [Integer] guest_port The port on the VM side. 14 | def initialize(host_port:, guest_port:) 15 | @host_port = host_port 16 | @guest_port = guest_port 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/orka_api_client/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | # Base error class. 5 | class Error < ::StandardError; end 6 | 7 | # This error is thrown if an endpoint requests an auth mechanism which we do not have credentials for. 8 | class AuthConfigurationError < Error; end 9 | 10 | # This error is thrown if a specific resource is requested but it was not found in the Orka backend. 11 | class ResourceNotFoundError < Error; end 12 | 13 | # This error is thrown if the client receives data from the server it does not recognise. This is typically 14 | # indicative of a bug or a feature not yet implemented. 15 | class UnrecognisedStateError < Error; end 16 | end 17 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/enumerator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # Enumerator subclass for networked operations. 6 | class Enumerator < ::Enumerator 7 | # @api private 8 | def initialize 9 | super do |yielder| 10 | yield.each do |item| 11 | yielder << item 12 | end 13 | end 14 | end 15 | 16 | # Forces this lazily-loaded enumerator to be fully loaded, performing any necessary network operations. 17 | # 18 | # @return [self] 19 | def eager 20 | begin 21 | peek 22 | rescue StopIteration 23 | # We're fine if the enuemrator is empty. 24 | end 25 | self 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/protocol_port_mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../port_mapping" 4 | 5 | module OrkaAPI 6 | module Models 7 | # Represents a port forwarding from a host node to a guest VM, with an additional field denoting the transport 8 | # protocol. 9 | class ProtocolPortMapping < PortMapping 10 | # @return [String] The transport protocol, typically TCP. 11 | attr_reader :protocol 12 | 13 | # @api private 14 | # @param [Integer] host_port 15 | # @param [Integer] guest_port 16 | # @param [String] protocol 17 | def initialize(host_port:, guest_port:, protocol:) 18 | super(host_port: host_port, guest_port: guest_port) 19 | @protocol = protocol 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/disk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # Information on a disk attached to a VM. 6 | class Disk 7 | # @return [String] 8 | attr_reader :type 9 | 10 | # @return [String] 11 | attr_reader :device 12 | 13 | # @return [String] 14 | attr_reader :target 15 | 16 | # @return [String] 17 | attr_reader :source 18 | 19 | # @api private 20 | # @param [String] type 21 | # @param [String] device 22 | # @param [String] target 23 | # @param [String] source 24 | def initialize(type:, device:, target:, source:) 25 | @type = type 26 | @device = device 27 | @target = target 28 | @source = source 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | analyze: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 27 | with: 28 | languages: ruby 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ["3.1", "3.0"] 19 | 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@d781c1b4ed31764801bfae177617bb0446f5ef8d # v1.218.0 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | bundler-cache: true 30 | 31 | - name: Build gem 32 | run: bundle exec rake 33 | 34 | - name: Run RuboCop 35 | run: bundle exec rake rubocop 36 | 37 | - name: Run tests 38 | run: bundle exec rake spec 39 | -------------------------------------------------------------------------------- /lib/orka_api_client/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "faraday" 4 | require "faraday/multipart" 5 | require_relative "auth_middleware" 6 | 7 | module OrkaAPI 8 | # @api private 9 | class Connection < ::Faraday::Connection 10 | # @param [String] base_url 11 | # @param [String] token 12 | # @param [String] license_key 13 | def initialize(base_url, token: nil, license_key: nil) 14 | super( 15 | url: base_url, 16 | headers: { 17 | "User-Agent" => "HomebrewOrkaClient/#{Client::VERSION}", 18 | }, 19 | request: { 20 | timeout: 600, 21 | } 22 | ) do |f| 23 | f.request :orka_auth, token: token, license_key: license_key 24 | f.request :json 25 | f.request :multipart 26 | f.response :json 27 | f.response :raise_error # TODO: wrap this ourselves 28 | end 29 | end 30 | 31 | alias inspect to_s 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/token_info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "attr_predicate" 4 | require_relative "user" 5 | 6 | module OrkaAPI 7 | module Models 8 | # Provides information on the client's token. 9 | class TokenInfo 10 | extend AttrPredicate 11 | 12 | # @return [Boolean] True if the tokeb is valid for authentication. 13 | attr_predicate :authenticated 14 | 15 | # @return [Boolean] True if the token has been revoked. 16 | attr_predicate :token_revoked 17 | 18 | # @return [User] The user associated with the token. 19 | attr_reader :user 20 | 21 | # @api private 22 | # @param [Hash] hash 23 | # @param [Connection] conn 24 | def initialize(hash, conn:) 25 | @authenticated = hash["authenticated"] 26 | @token_revoked = hash["is_token_revoked"] 27 | @user = Models::User.lazy_prepare(email: hash["email"], conn: conn) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/orka_api_client/auth_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | # @api private 5 | class AuthMiddleware < ::Faraday::Middleware 6 | def initialize(app, token: nil, license_key: nil) 7 | super(app) 8 | 9 | @token = token 10 | @license_key = license_key 11 | end 12 | 13 | def on_request(env) 14 | auth_type = env.request.context&.dig(:orka_auth_type) 15 | 16 | Array(auth_type).each do |type| 17 | case type 18 | when :license 19 | header = "orka-licensekey" 20 | value = @license_key 21 | when :token 22 | header = "Authorization" 23 | value = "Bearer #{@token}" 24 | when nil, :none 25 | next 26 | else 27 | raise AuthConfigurationError, "Invalid Orka auth type." 28 | end 29 | 30 | raise AuthConfigurationError, "Missing #{type} credential." if value.nil? 31 | 32 | next if env.request_headers[header] 33 | 34 | env.request_headers[header] = value 35 | end 36 | end 37 | end 38 | end 39 | 40 | Faraday::Request.register_middleware(orka_auth: OrkaAPI::AuthMiddleware) 41 | -------------------------------------------------------------------------------- /orka_api_client.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/orka_api_client/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "orka_api_client" 7 | spec.version = OrkaAPI::Client::VERSION 8 | spec.authors = ["Bo Anderson"] 9 | spec.email = ["mail@boanderson.me"] 10 | 11 | spec.summary = "Ruby library for interfacing with the MacStadium Orka API." 12 | spec.homepage = "https://github.com/Homebrew/orka_api_client" 13 | spec.license = "BSD-2-Clause" 14 | spec.required_ruby_version = ">= 3.0.0" 15 | 16 | spec.metadata["homepage_uri"] = spec.homepage 17 | spec.metadata["source_code_uri"] = "https://github.com/Homebrew/orka_api_client" 18 | spec.metadata["bug_tracker_uri"] = "https://github.com/Homebrew/orka_api_client/issues" 19 | spec.metadata["changelog_uri"] = "https://github.com/Homebrew/orka_api_client/releases/tag/#{spec.version}" 20 | spec.metadata["rubygems_mfa_required"] = "true" 21 | 22 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 23 | (Dir.glob("*.{md,txt}") + Dir.glob("{exe,lib,rbi}/**/*")).reject { |f| File.directory?(f) } 24 | end 25 | spec.bindir = "exe" 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | 29 | spec.add_dependency "faraday", "~> 2.0" 30 | spec.add_dependency "faraday-multipart", "~> 1.0" 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022-present, Homebrew contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/remote_image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # Represents an image which exists in the Orka remote repo rather than local storage. 6 | class RemoteImage 7 | # @return [String] The name of this remote image. 8 | attr_reader :name 9 | 10 | # @api private 11 | # @param [String] name 12 | # @param [Connection] conn 13 | def initialize(name, conn:) 14 | @name = name 15 | @conn = conn 16 | end 17 | 18 | # Pull this image from the remote repo. This is a long-running operation and might take a while. 19 | # 20 | # The operation copies the image to the local storage of your Orka environment. The base image will be 21 | # available for use by all users of the environment. 22 | # 23 | # @macro auth_token 24 | # 25 | # @param [String] new_name The name for the local copy of this image. 26 | # @return [Image] The lazily-loaded local image. 27 | def pull(new_name) 28 | body = { 29 | image: @name, 30 | new_name: new_name, 31 | }.compact 32 | @conn.post("resources/image/pull", body) do |r| 33 | r.options.context = { 34 | orka_auth_type: :token, 35 | } 36 | end 37 | Image.lazy_prepare(name: new_name, conn: @conn) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/remote_iso.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # Represents an ISO which exists in the Orka remote repo rather than local storage. 6 | class RemoteISO 7 | # @return [String] The name of this remote ISO. 8 | attr_reader :name 9 | 10 | # @api private 11 | # @param [String] name 12 | # @param [Connection] conn 13 | def initialize(name, conn:) 14 | @name = name 15 | @conn = conn 16 | end 17 | 18 | # Pull an ISO from the remote repo. You can retain the ISO name or change it during the operation. This is a 19 | # long-running operation and might take a while. 20 | # 21 | # The operation copies the ISO to the local storage of your Orka environment. The ISO will be available for use 22 | # by all users of the environment. 23 | # 24 | # @macro auth_token 25 | # 26 | # @param [String] new_name The name for the local copy of this ISO. 27 | # @return [ISO] The lazily-loaded local ISO. 28 | def pull(new_name) 29 | body = { 30 | image: @name, 31 | new_name: new_name, 32 | }.compact 33 | @conn.post("resources/iso/pull", body) do |r| 34 | r.options.context = { 35 | orka_auth_type: :token, 36 | } 37 | end 38 | ISO.lazy_prepare(name: new_name, conn: @conn) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/lazy_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # The base class for lazily-loaded objects. 6 | class LazyModel 7 | # @!macro [attach] lazy_attr 8 | # @!attribute [r] 9 | def self.lazy_attr(*attrs) 10 | attrs.each do |attr| 11 | define_method attr do 12 | ivar = "@#{attr.to_s.delete_suffix("?")}" 13 | existing = instance_variable_get(ivar) 14 | return existing unless existing.nil? 15 | 16 | eager 17 | instance_variable_get(ivar) 18 | end 19 | end 20 | end 21 | private_class_method :lazy_attr 22 | 23 | # @private 24 | # @param [Boolean] lazy_initialized 25 | def initialize(lazy_initialized) 26 | @lazy_initialized = lazy_initialized 27 | end 28 | private_class_method :new 29 | 30 | # Forces this lazily-loaded object to be fully loaded, performing any necessary network operations. 31 | # 32 | # @return [self] 33 | def eager 34 | lazy_initialize unless @lazy_initialized 35 | self 36 | end 37 | 38 | # Re-fetches this object's data from the Orka API. This will raise an error if the object no longer exists. 39 | # 40 | # @return [void] 41 | def refresh 42 | lazy_initialize 43 | end 44 | 45 | private 46 | 47 | def lazy_initialize 48 | @lazy_initialized = true 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # orka_api_client 2 | 3 | This is a Ruby library for interacting with MacStadium's [Orka](https://www.macstadium.com/orka) API. 4 | 5 | ⚠️⚠️ **This gem is largely untested beyond read-only operations. API stability is not yet guaranteed.** ⚠️⚠️ 6 | 7 | ## Installation 8 | 9 | **This gem is not yet available on RubyGems.** 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'orka_api_client', git: "https://github.com/Homebrew/orka_api_client" 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle install 20 | 21 | ## Usage 22 | 23 | TODO: examples 24 | 25 | Documentation is in the `docs` folder after running `bundle exec rake yard`. 26 | 27 | A Sorbet RBI file is available for this gem. 28 | 29 | ## Development 30 | 31 | After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 32 | 33 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 34 | 35 | RuboCop can be run via `bundle exec rake rubocop`. 36 | 37 | ## Tests 38 | 39 | This is non-existent at the moment. Ideally this would involve a real Orka test environment, but I don't have one readily available that's not already being used for real CI. 40 | 41 | When they exist, `bundle exec rake spec` can be used to run the tests. 42 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | - rubocop-rake 4 | - rubocop-rspec 5 | 6 | AllCops: 7 | NewCops: enable 8 | 9 | Layout/CaseIndentation: 10 | EnforcedStyle: end 11 | 12 | Layout/EndAlignment: 13 | EnforcedStyleAlignWith: start_of_line 14 | 15 | Layout/FirstArrayElementIndentation: 16 | EnforcedStyle: consistent 17 | 18 | Layout/FirstHashElementIndentation: 19 | EnforcedStyle: consistent 20 | 21 | Layout/HashAlignment: 22 | EnforcedHashRocketStyle: table 23 | EnforcedColonStyle: table 24 | 25 | Layout/LineLength: 26 | Max: 118 27 | 28 | Metrics/AbcSize: 29 | Max: 60 30 | 31 | Metrics/BlockLength: 32 | Max: 50 33 | 34 | Metrics/ClassLength: 35 | Max: 500 36 | 37 | Metrics/CyclomaticComplexity: 38 | Max: 20 39 | 40 | Metrics/PerceivedComplexity: 41 | Max: 20 42 | 43 | Metrics/MethodLength: 44 | Max: 50 45 | 46 | Metrics/ParameterLists: 47 | CountKeywordArgs: false 48 | 49 | Style/AndOr: 50 | EnforcedStyle: always 51 | 52 | Style/AutoResourceCleanup: 53 | Enabled: true 54 | 55 | Style/CollectionMethods: 56 | Enabled: true 57 | 58 | Style/MutableConstant: 59 | EnforcedStyle: strict 60 | 61 | Style/StringLiterals: 62 | EnforcedStyle: double_quotes 63 | 64 | Style/StringLiteralsInInterpolation: 65 | EnforcedStyle: double_quotes 66 | 67 | Style/StringMethods: 68 | Enabled: true 69 | 70 | Style/SymbolArray: 71 | EnforcedStyle: brackets 72 | 73 | Style/TernaryParentheses: 74 | EnforcedStyle: require_parentheses_when_complex 75 | 76 | Style/TrailingCommaInArguments: 77 | EnforcedStyleForMultiline: comma 78 | 79 | Style/TrailingCommaInArrayLiteral: 80 | EnforcedStyleForMultiline: comma 81 | 82 | Style/TrailingCommaInHashLiteral: 83 | EnforcedStyleForMultiline: comma 84 | 85 | Style/UnlessLogicalOperators: 86 | Enabled: true 87 | EnforcedStyle: forbid_logical_operators 88 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/kube_account.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OrkaAPI 4 | module Models 5 | # An account used for Kubernetes operations. 6 | class KubeAccount 7 | # @return [String] The name of this kube-account. 8 | attr_reader :name 9 | 10 | # @api private 11 | # @param [String] name 12 | # @param [String] email 13 | # @param [String] kubeconfig 14 | # @param [Connection] conn 15 | def initialize(name, conn:, email: nil, kubeconfig: nil) 16 | @name = name 17 | @email = email 18 | @kubeconfig = kubeconfig 19 | @conn = conn 20 | end 21 | 22 | # Regenerate this kube-account. 23 | # 24 | # @macro auth_token_and_license 25 | # 26 | # @return [void] 27 | def regenerate 28 | body = { 29 | name: name, 30 | email: email, 31 | }.compact 32 | @kubeconfig = @conn.post("resources/kube-account/regenerate", body) do |r| 33 | r.options.context = { 34 | orka_auth_type: [:token, :license], 35 | } 36 | end.body["kubeconfig"] 37 | end 38 | 39 | # Retrieve the +kubeconfig+ for this kube-account. 40 | # 41 | # This method is cached. Subsequent calls to this method will not invoke additional network requests. The 42 | # methods {#regenerate} and {Client#create_kube_account} also fill this cache. 43 | # 44 | # @macro auth_token_and_license 45 | # 46 | # @return [void] 47 | def kubeconfig 48 | return @kubeconfig unless @kubeconfig.nil? 49 | 50 | @kubeconfig = @conn.get("resources/kube-account/download") do |r| 51 | r.body = { 52 | name: @name, 53 | email: @email, 54 | }.compact 55 | 56 | r.options.context = { 57 | orka_auth_type: [:token, :license], 58 | } 59 | end.body["@kubeconfig"] 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | name: Actionlint 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | merge_group: 11 | 12 | defaults: 13 | run: 14 | shell: bash -xeuo pipefail {0} 15 | 16 | concurrency: 17 | group: "actionlint-${{ github.ref }}" 18 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 19 | 20 | env: 21 | HOMEBREW_DEVELOPER: 1 22 | HOMEBREW_NO_AUTO_UPDATE: 1 23 | HOMEBREW_NO_ENV_HINTS: 1 24 | 25 | permissions: {} 26 | 27 | jobs: 28 | workflow_syntax: 29 | if: github.repository_owner == 'Homebrew' 30 | runs-on: ubuntu-latest 31 | permissions: 32 | contents: read 33 | steps: 34 | - name: Set up Homebrew 35 | id: setup-homebrew 36 | uses: Homebrew/actions/setup-homebrew@master 37 | with: 38 | core: false 39 | cask: false 40 | test-bot: false 41 | 42 | - name: Install tools 43 | run: brew install actionlint shellcheck zizmor 44 | 45 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | with: 47 | persist-credentials: false 48 | 49 | - run: zizmor --format sarif . > results.sarif 50 | 51 | - name: Upload SARIF file 52 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 53 | with: 54 | name: results.sarif 55 | path: results.sarif 56 | 57 | - name: Set up actionlint 58 | run: echo "::add-matcher::$(brew --repository)/.github/actionlint-matcher.json" 59 | 60 | - run: actionlint 61 | 62 | upload_sarif: 63 | needs: workflow_syntax 64 | # We want to always upload this even if `actionlint` failed. 65 | # This is only available on public repositories. 66 | if: > 67 | always() && 68 | !contains(fromJSON('["cancelled", "skipped"]'), needs.workflow_syntax.result) && 69 | !github.event.repository.private 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: read 73 | security-events: write 74 | steps: 75 | - name: Download SARIF file 76 | uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 77 | with: 78 | name: results.sarif 79 | path: results.sarif 80 | 81 | - name: Upload SARIF file 82 | uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9 83 | with: 84 | sarif_file: results.sarif 85 | category: zizmor 86 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/vm_deployment_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "attr_predicate" 4 | 5 | module OrkaAPI 6 | module Models 7 | # Provides information on the just-deployed VM. 8 | class VMDeploymentResult 9 | extend AttrPredicate 10 | 11 | # @return [String] The amount of RAM allocated to the VM. 12 | attr_reader :ram 13 | 14 | # @return [Integer] The number of vCPUs allocated to the VM. 15 | attr_reader :vcpu_count 16 | 17 | # @return [Integer] The number of host CPU cores allocated to the VM. 18 | attr_reader :cpu_cores 19 | 20 | # @return [String] The IP of the VM. 21 | attr_reader :ip 22 | 23 | # @return [Integer] The port used to connect to the VM via SSH. 24 | attr_reader :ssh_port 25 | 26 | # @return [Integer] The port used to connect to the VM via macOS Screen Sharing. 27 | attr_reader :screen_sharing_port 28 | 29 | # @return [VMResource] The VM resource object representing this VM. 30 | attr_reader :resource 31 | 32 | # @return [Boolean] True if IO boost is enabled for this VM. 33 | attr_predicate :io_boost 34 | 35 | # @return [Boolean] True if network boost is enabled for this VM. 36 | attr_predicate :io_boost 37 | 38 | # @return [Boolean] True if this VM is using a prior saved state rather than a clean base image. 39 | attr_predicate :use_saved_state 40 | 41 | # @return [Boolean] True if GPU passthrough is enabled for this VM. 42 | attr_predicate :gpu_passthrough 43 | 44 | # @return [Integer, nil] The port used to connect to the VM via VNC, if enabled. 45 | attr_reader :vnc_port 46 | 47 | # @api private 48 | # @param [Hash] hash 49 | # @param [Connection] conn 50 | # @param [Boolean] admin 51 | def initialize(hash, conn:, admin: false) 52 | @ram = hash["ram"] 53 | @vcpu_count = hash["vcpu"].to_i 54 | @cpu_cores = hash["host_cpu"].to_i 55 | @ip = hash["ip"] 56 | @ssh_port = hash["ssh_port"].to_i 57 | @screen_sharing_port = hash["screen_share_port"].to_i 58 | @resource = Models::VMResource.lazy_prepare(name: hash["vm_id"], conn: conn, admin: admin) 59 | # TODO: port_warnings? 60 | @io_boost = hash["io_boost"] 61 | @net_boost = hash["net_boost"] 62 | @use_saved_state = if hash["use_saved_state"] == "N/A" 63 | false 64 | else 65 | hash["use_saved_state"] 66 | end 67 | @gpu_passthrough = if hash["gpu_passthrough"] == "N/A" 68 | false 69 | else 70 | hash["gpu_passthrough"] 71 | end 72 | @vnc_port = if hash["vnc_port"] == "N/A" 73 | nil 74 | else 75 | hash["vnc_port"].to_i 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | orka_api_client (0.2.1) 5 | faraday (~> 2.0) 6 | faraday-multipart (~> 1.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | ast (2.4.2) 12 | commander (4.6.0) 13 | highline (~> 2.0.0) 14 | diff-lcs (1.6.0) 15 | faraday (2.12.2) 16 | faraday-net_http (>= 2.0, < 3.5) 17 | json 18 | logger 19 | faraday-multipart (1.1.0) 20 | multipart-post (~> 2.0) 21 | faraday-net_http (3.4.0) 22 | net-http (>= 0.5.0) 23 | highline (2.0.3) 24 | json (2.10.1) 25 | language_server-protocol (3.17.0.4) 26 | logger (1.6.6) 27 | multipart-post (2.4.1) 28 | net-http (0.6.0) 29 | uri 30 | parallel (1.26.3) 31 | parlour (5.0.0) 32 | commander (~> 4.5) 33 | parser 34 | rainbow (~> 3.0) 35 | sorbet-runtime (>= 0.5) 36 | parser (3.3.7.1) 37 | ast (~> 2.4.1) 38 | racc 39 | racc (1.8.1) 40 | rainbow (3.1.1) 41 | rake (13.2.1) 42 | rbs (3.6.1) 43 | logger 44 | regexp_parser (2.10.0) 45 | rspec (3.13.0) 46 | rspec-core (~> 3.13.0) 47 | rspec-expectations (~> 3.13.0) 48 | rspec-mocks (~> 3.13.0) 49 | rspec-core (3.13.3) 50 | rspec-support (~> 3.13.0) 51 | rspec-expectations (3.13.3) 52 | diff-lcs (>= 1.2.0, < 2.0) 53 | rspec-support (~> 3.13.0) 54 | rspec-mocks (3.13.2) 55 | diff-lcs (>= 1.2.0, < 2.0) 56 | rspec-support (~> 3.13.0) 57 | rspec-support (3.13.2) 58 | rubocop (1.71.2) 59 | json (~> 2.3) 60 | language_server-protocol (>= 3.17.0) 61 | parallel (~> 1.10) 62 | parser (>= 3.3.0.2) 63 | rainbow (>= 2.2.2, < 4.0) 64 | regexp_parser (>= 2.9.3, < 3.0) 65 | rubocop-ast (>= 1.38.0, < 2.0) 66 | ruby-progressbar (~> 1.7) 67 | unicode-display_width (>= 2.4.0, < 4.0) 68 | rubocop-ast (1.38.0) 69 | parser (>= 3.3.1.0) 70 | rubocop-performance (1.23.1) 71 | rubocop (>= 1.48.1, < 2.0) 72 | rubocop-ast (>= 1.31.1, < 2.0) 73 | rubocop-rake (0.6.0) 74 | rubocop (~> 1.0) 75 | rubocop-rspec (3.4.0) 76 | rubocop (~> 1.61) 77 | ruby-progressbar (1.13.0) 78 | sorbet-runtime (0.5.11826) 79 | sord (6.0.0) 80 | commander (~> 4.5) 81 | parlour (~> 5.0) 82 | rbs (~> 3.0) 83 | sorbet-runtime 84 | yard 85 | unicode-display_width (3.1.4) 86 | unicode-emoji (~> 4.0, >= 4.0.4) 87 | unicode-emoji (4.0.4) 88 | uri (1.0.2) 89 | yard (0.9.37) 90 | 91 | PLATFORMS 92 | ruby 93 | 94 | DEPENDENCIES 95 | orka_api_client! 96 | parlour 97 | rake 98 | rspec 99 | rubocop 100 | rubocop-performance 101 | rubocop-rake 102 | rubocop-rspec 103 | sord (~> 6.0) 104 | yard 105 | 106 | BUNDLED WITH 107 | 2.5.20 108 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from the `.github` repository, do not modify it directly. 2 | name: Manage stale issues 3 | 4 | on: 5 | push: 6 | paths: 7 | - .github/workflows/stale-issues.yml 8 | branches-ignore: 9 | - dependabot/** 10 | schedule: 11 | # Once every day at midnight UTC 12 | - cron: "0 0 * * *" 13 | issue_comment: 14 | 15 | permissions: {} 16 | 17 | defaults: 18 | run: 19 | shell: bash -xeuo pipefail {0} 20 | 21 | concurrency: 22 | group: stale-issues 23 | cancel-in-progress: ${{ github.event_name != 'issue_comment' }} 24 | 25 | jobs: 26 | stale: 27 | if: > 28 | github.repository_owner == 'Homebrew' && ( 29 | github.event_name != 'issue_comment' || ( 30 | contains(github.event.issue.labels.*.name, 'stale') || 31 | contains(github.event.pull_request.labels.*.name, 'stale') 32 | ) 33 | ) 34 | runs-on: ubuntu-latest 35 | permissions: 36 | contents: write 37 | issues: write 38 | pull-requests: write 39 | steps: 40 | - name: Mark/Close Stale Issues and Pull Requests 41 | uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 42 | with: 43 | repo-token: ${{ secrets.GITHUB_TOKEN }} 44 | days-before-stale: 21 45 | days-before-close: 7 46 | stale-issue-message: > 47 | This issue has been automatically marked as stale because it has not had 48 | recent activity. It will be closed if no further activity occurs. 49 | stale-pr-message: > 50 | This pull request has been automatically marked as stale because it has not had 51 | recent activity. It will be closed if no further activity occurs. 52 | exempt-issue-labels: "gsoc-outreachy,help wanted,in progress" 53 | exempt-pr-labels: "gsoc-outreachy,help wanted,in progress" 54 | delete-branch: true 55 | 56 | bump-pr-stale: 57 | if: > 58 | github.repository_owner == 'Homebrew' && ( 59 | github.event_name != 'issue_comment' || ( 60 | contains(github.event.issue.labels.*.name, 'stale') || 61 | contains(github.event.pull_request.labels.*.name, 'stale') 62 | ) 63 | ) 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: write 67 | issues: write 68 | pull-requests: write 69 | steps: 70 | - name: Mark/Close Stale `bump-formula-pr` and `bump-cask-pr` Pull Requests 71 | uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 72 | with: 73 | repo-token: ${{ secrets.GITHUB_TOKEN }} 74 | days-before-stale: 2 75 | days-before-close: 1 76 | stale-pr-message: > 77 | This pull request has been automatically marked as stale because it has not had 78 | recent activity. It will be closed if no further activity occurs. To keep this 79 | pull request open, add a `help wanted` or `in progress` label. 80 | exempt-pr-labels: "help wanted,in progress" 81 | any-of-labels: "bump-formula-pr,bump-cask-pr" 82 | delete-branch: true 83 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | 5 | module OrkaAPI 6 | module Models 7 | # To work with Orka, you need to have a user with an assigned license. You will use this user and the respective 8 | # credentials to authenticate against the Orka service. After being authenticated against the service, you can 9 | # run Orka API calls. 10 | class User < LazyModel 11 | # @return [String] The email address for the user. 12 | attr_reader :email 13 | 14 | # @return [String, nil] The group the user is in, if any. 15 | lazy_attr :group 16 | 17 | # @api private 18 | # @param [String] email 19 | # @param [Connection] conn 20 | # @return [User] 21 | def self.lazy_prepare(email:, conn:) 22 | new(conn: conn, email: email) 23 | end 24 | 25 | # @api private 26 | # @param [Connection] conn 27 | # @param [String] email 28 | # @param [String] group 29 | def initialize(conn:, email:, group: nil) 30 | super(!group.nil?) 31 | @conn = conn 32 | @email = email 33 | @group = if group == "$ungrouped" 34 | nil 35 | else 36 | group 37 | end 38 | end 39 | public_class_method :new 40 | 41 | # Delete the user in the endpoint. The user must have no Orka resources associated with them (other than their 42 | # authentication tokens). This operation invalidates all tokens associated with the user. 43 | # 44 | # @macro auth_token_and_license 45 | # 46 | # @return [void] 47 | def delete 48 | @conn.delete("users/#{@email}") do |r| 49 | r.options.context = { 50 | orka_auth_type: [:license, :token], 51 | } 52 | end 53 | end 54 | 55 | # Reset the password for the user. This operation is intended for administrators. 56 | # 57 | # @macro auth_token_and_license 58 | # 59 | # @param [String] password The new password for the user. 60 | # @return [void] 61 | def reset_password(password) 62 | body = { 63 | email: email, 64 | password: password, 65 | }.compact 66 | @conn.post("users/password", body) do |r| 67 | r.options.context = { 68 | orka_auth_type: [:license, :token], 69 | } 70 | end 71 | end 72 | 73 | # Apply a group label to the user. 74 | # 75 | # @note This is a BETA feature. 76 | # 77 | # @macro auth_license 78 | # 79 | # @param [String] group The new group for the user. 80 | # @return [void] 81 | def change_group(group) 82 | @conn.post("users/groups/#{group || "$ungrouped"}", [@email]) do |r| 83 | r.options.context = { 84 | orka_auth_type: :license, 85 | } 86 | end 87 | @group = group 88 | end 89 | 90 | # Remove a group label from the user. 91 | # 92 | # @note This is a BETA feature. 93 | # 94 | # @macro auth_license 95 | # 96 | # @return [void] 97 | def remove_group 98 | change_group(nil) 99 | end 100 | 101 | private 102 | 103 | def lazy_initialize 104 | body = @conn.get("users") do |r| 105 | r.options.context = { 106 | orka_auth_type: :license, 107 | } 108 | end.body 109 | groups = body["user_groups"] 110 | 111 | group = if groups.nil? 112 | user_list = body["user_list"] 113 | raise ResourceNotFoundError, "No user found matching \"#{@email}\"." unless user_list.include?(@email) 114 | 115 | "$ungrouped" 116 | else 117 | group = groups.find { |_, group_users| group_users.include?(@email) }&.first 118 | raise ResourceNotFoundError, "No user found matching \"#{@email}\"." if group.nil? 119 | 120 | group 121 | end 122 | 123 | @group = if group == "$ungrouped" 124 | nil 125 | else 126 | group 127 | end 128 | super 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/iso.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | 5 | module OrkaAPI 6 | module Models 7 | # An +.iso+ disk image used exclusively for the installation of macOS on a virtual machine. You must attach the 8 | # ISO to the VM during deployment. After the installation is complete and the VM has booted successfully, you 9 | # need to restart the VM to detach the ISO. 10 | # 11 | # @note All ISO requests are supported for Intel nodes only. 12 | class ISO < LazyModel 13 | # @return [String] The name of this ISO. 14 | attr_reader :name 15 | 16 | # @return [String] The size of this ISO. 17 | lazy_attr :size 18 | 19 | # @return [DateTime] The time this image was last modified. 20 | lazy_attr :modification_time 21 | 22 | # @api private 23 | # @param [String] name 24 | # @param [Connection] conn 25 | # @return [ISO] 26 | def self.lazy_prepare(name:, conn:) 27 | new(conn: conn, name: name) 28 | end 29 | 30 | # @api private 31 | # @param [Hash] hash 32 | # @param [Connection] conn 33 | # @return [ISO] 34 | def self.from_hash(hash, conn:) 35 | new(conn: conn, hash: hash) 36 | end 37 | 38 | # @private 39 | # @param [Connection] conn 40 | # @param [String] name 41 | # @param [Hash] hash 42 | def initialize(conn:, name: nil, hash: nil) 43 | super(!hash.nil?) 44 | @conn = conn 45 | @name = name 46 | deserialize(hash) if hash 47 | end 48 | 49 | # Rename this ISO. 50 | # 51 | # @macro auth_token 52 | # 53 | # @note Make sure that the ISO is not in use. Any VMs that have the ISO of the old name attached will no longer 54 | # be able to boot from it. 55 | # 56 | # @param [String] new_name The new name for this ISO. 57 | # @return [void] 58 | def rename(new_name) 59 | body = { 60 | iso: @name, 61 | new_name: new_name, 62 | }.compact 63 | @conn.post("resources/iso/rename", body) do |r| 64 | r.options.context = { 65 | orka_auth_type: :token, 66 | } 67 | end 68 | @name = new_name 69 | end 70 | 71 | # Copy this ISO to a new one. 72 | # 73 | # @macro auth_token 74 | # 75 | # @param [String] new_name The name for the copy of this ISO. 76 | # @return [Image] The lazily-loaded ISO copy. 77 | def copy(new_name) 78 | body = { 79 | iso: @name, 80 | new_name: new_name, 81 | }.compact 82 | @conn.post("resources/iso/copy", body) do |r| 83 | r.options.context = { 84 | orka_auth_type: :token, 85 | } 86 | end 87 | ISO.lazy_prepare(new_name, conn: @conn) 88 | end 89 | 90 | # Delete this ISO from the local Orka storage. 91 | # 92 | # @macro auth_token 93 | # 94 | # @note Make sure that the ISO is not in use. Any VMs that have the ISO attached will no longer be able to boot 95 | # from it. 96 | # 97 | # @return [void] 98 | def delete 99 | body = { 100 | iso: @name, 101 | }.compact 102 | @conn.post("resources/iso/delete", body) do |r| 103 | r.options.context = { 104 | orka_auth_type: :token, 105 | } 106 | end 107 | end 108 | 109 | private 110 | 111 | def lazy_initialize 112 | response = @conn.get("resources/iso/list") do |r| 113 | r.options.context = { 114 | orka_auth_type: :token, 115 | } 116 | end 117 | iso = response.body["iso_attributes"].find { |hash| hash["iso"] == @name } 118 | 119 | raise ResourceNotFoundError, "No ISO found matching \"#{@name}\"." if iso.nil? 120 | 121 | deserialize(iso) 122 | super 123 | end 124 | 125 | def deserialize(hash) 126 | @name = hash["iso"] 127 | @size = hash["iso_size"] 128 | @modification_time = DateTime.iso8601(hash["modified"]) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | 5 | module OrkaAPI 6 | module Models 7 | # A disk image that represents a VM's storage and its contents, including the OS and any installed software. 8 | class Image < LazyModel 9 | # @return [String] The name of this image. 10 | attr_reader :name 11 | 12 | # @return [String] The size of this image. Orka lists generated empty storage disks with a fixed size of ~192k. 13 | # When attached to a VM and formatted, the disk will appear with its correct size in the OS. 14 | lazy_attr :size 15 | 16 | # @return [DateTime] The time this image was last modified. 17 | lazy_attr :modification_time 18 | 19 | # @return [DateTime, nil] The time this image was first created, if available. 20 | lazy_attr :creation_time 21 | 22 | # @return [String] 23 | lazy_attr :owner 24 | 25 | # @api private 26 | # @param [String] name 27 | # @param [Connection] conn 28 | # @return [Image] 29 | def self.lazy_prepare(name:, conn:) 30 | new(conn: conn, name: name) 31 | end 32 | 33 | # @api private 34 | # @param [Hash] hash 35 | # @param [Connection] conn 36 | # @return [Image] 37 | def self.from_hash(hash, conn:) 38 | new(conn: conn, hash: hash) 39 | end 40 | 41 | # @private 42 | # @param [Connection] conn 43 | # @param [String] name 44 | # @param [Hash] hash 45 | def initialize(conn:, name: nil, hash: nil) 46 | super(!hash.nil?) 47 | @conn = conn 48 | @name = name 49 | deserialize(hash) if hash 50 | end 51 | 52 | # Rename this image. 53 | # 54 | # @macro auth_token 55 | # 56 | # @note After you rename a base image, you can no longer deploy any VM configurations that are based on the 57 | # image of the old name. 58 | # 59 | # @param [String] new_name The new name for this image. 60 | # @return [void] 61 | def rename(new_name) 62 | body = { 63 | image: @name, 64 | new_name: new_name, 65 | }.compact 66 | @conn.post("resources/image/rename", body) do |r| 67 | r.options.context = { 68 | orka_auth_type: :token, 69 | } 70 | end 71 | @name = new_name 72 | end 73 | 74 | # Copy this image to a new one. 75 | # 76 | # @macro auth_token 77 | # 78 | # @param [String] new_name The name for the copy of this image. 79 | # @return [Image] The lazily-loaded image copy. 80 | def copy(new_name) 81 | body = { 82 | image: @name, 83 | new_name: new_name, 84 | }.compact 85 | @conn.post("resources/image/copy", body) do |r| 86 | r.options.context = { 87 | orka_auth_type: :token, 88 | } 89 | end 90 | Image.lazy_prepare(name: new_name, conn: @conn) 91 | end 92 | 93 | # Delete this image from the local Orka storage. 94 | # 95 | # @macro auth_token 96 | # 97 | # @note Make sure that the image is not in use. 98 | # 99 | # @return [void] 100 | def delete 101 | body = { 102 | image: @name, 103 | }.compact 104 | @conn.post("resources/image/delete", body) do |r| 105 | r.options.context = { 106 | orka_auth_type: :token, 107 | } 108 | end 109 | end 110 | 111 | # Download this image from Orka cluster storage to your local filesystem. 112 | # 113 | # @macro auth_token 114 | # 115 | # @note This request is supported for Intel images only. Intel images have +.img+ extension. 116 | # 117 | # @param [String, Pathname, IO] to An open IO, or a String/Pathname file path to the file or directory where 118 | # you want the image to be written. 119 | # @return [void] 120 | def download(to:) 121 | io_input = to.is_a?(::IO) 122 | file = if io_input 123 | to 124 | else 125 | to = File.join(to, @name) if File.directory?(to) 126 | File.open(to, "wb:ASCII-8BIT") 127 | end 128 | @conn.get("resources/image/download/#{@name}") do |r| 129 | r.options.context = { 130 | orka_auth_type: :token, 131 | } 132 | 133 | r.options.on_data = proc do |chunk, _| 134 | file.write(chunk) 135 | end 136 | end 137 | ensure 138 | file.close unless io_input 139 | end 140 | 141 | # Request the MD5 file checksum for this image in Orka cluster storage. The checksum can be used to verify file 142 | # integrity for a downloaded or uploaded image. 143 | # 144 | # @macro auth_token 145 | # 146 | # @note This request is supported for Intel images only. Intel images have +.img+ extension. 147 | # 148 | # @return [String, nil] The MD5 checksum of the image, or nil if the calculation is in progress and has not 149 | # completed. 150 | def checksum 151 | @conn.get("resources/image/checksum/#{@name}") do |r| 152 | r.options.context = { 153 | orka_auth_type: :token, 154 | } 155 | end.body&.dig("checksum") 156 | end 157 | 158 | private 159 | 160 | def lazy_initialize 161 | response = @conn.get("resources/image/list") do |r| 162 | r.options.context = { 163 | orka_auth_type: :token, 164 | } 165 | end 166 | image = response.body["image_attributes"].find { |hash| hash["image"] == @name } 167 | 168 | raise ResourceNotFoundError, "No image found matching \"#{@name}\"." if image.nil? 169 | 170 | deserialize(image) 171 | super 172 | end 173 | 174 | def deserialize(hash) 175 | @name = hash["image"] 176 | @size = hash["image_size"] 177 | @modification_time = DateTime.iso8601(hash["modified"]) 178 | @creation_time = if hash["date_added"] == "N/A" 179 | nil 180 | else 181 | DateTime.iso8601(hash["date_added"]) 182 | end 183 | @owner = hash["owner"] 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/node.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | require_relative "protocol_port_mapping" 5 | 6 | module OrkaAPI 7 | module Models 8 | # A physical or logical host that provides computational resources for your VMs. Usually, an Orka node is a 9 | # genuine Apple physical host with a host OS on top. You have no direct access (via VNC, SSH, or Screen Sharing) 10 | # to your nodes. 11 | class Node < LazyModel 12 | # @return [String] The name of this node. 13 | attr_reader :name 14 | 15 | # @return [String] The host name of this node. 16 | lazy_attr :host_name 17 | 18 | # @return [String] The IP address of this node. 19 | lazy_attr :address 20 | 21 | # @return [String] The host IP address of this node. 22 | lazy_attr :host_ip 23 | 24 | # @return [Integer] The number of free CPU cores on this node. 25 | lazy_attr :available_cpu_cores 26 | 27 | # @return [Integer] The total number of CPU cores on this node that are allocatable to VMs. 28 | lazy_attr :allocatable_cpu_cores 29 | 30 | # @return [Integer] The number of free GPUs on this node. 31 | lazy_attr :available_gpu_count 32 | 33 | # @return [Integer] The total number of GPUs on this node that are allocatable to VMs. 34 | lazy_attr :allocatable_gpu_count 35 | 36 | # @return [String] The amount of free RAM on this node. 37 | lazy_attr :available_memory 38 | 39 | # @return [Integer] The total number of CPU cores on this node. 40 | lazy_attr :total_cpu_cores 41 | 42 | # @return [String] The total amount of RAM on this node. 43 | lazy_attr :total_memory 44 | 45 | # @return [String] The type of this node (WORKER, FOUNDATION, SANDBOX). 46 | lazy_attr :type 47 | 48 | # @return [String] The state of this node. 49 | lazy_attr :state 50 | 51 | # @return [String, nil] The user group this node is dedicated to, if any. 52 | lazy_attr :orka_group 53 | 54 | # @return [Array] The list of tags this node has been assigned. 55 | lazy_attr :tags 56 | 57 | # @api private 58 | # @param [String] name 59 | # @param [Connection] conn 60 | # @param [Boolean] admin 61 | # @return [Node] 62 | def self.lazy_prepare(name:, conn:, admin: false) 63 | new(conn: conn, name: name, admin: admin) 64 | end 65 | 66 | # @api private 67 | # @param [Hash] hash 68 | # @param [Connection] conn 69 | # @param [Boolean] admin 70 | # @return [Node] 71 | def self.from_hash(hash, conn:, admin: false) 72 | new(conn: conn, hash: hash, admin: admin) 73 | end 74 | 75 | # @private 76 | # @param [Connection] conn 77 | # @param [String] name 78 | # @param [Hash] hash 79 | # @param [Boolean] admin 80 | def initialize(conn:, name: nil, hash: nil, admin: false) 81 | super(!hash.nil?) 82 | @conn = conn 83 | @name = name 84 | @admin = admin 85 | deserialize(hash) if hash 86 | end 87 | 88 | # Get a detailed list of all reserved ports on this node. Orka lists them as port mappings between 89 | # {ProtocolPortMapping#host_port host_port} and {ProtocolPortMapping#guest_port guest_port}. 90 | # {ProtocolPortMapping#host_port host_port} indicates a port on the node, {ProtocolPortMapping#guest_port 91 | # guest_port} indicates a port on a VM on this node. 92 | # 93 | # @macro auth_token 94 | # 95 | # @macro lazy_enumerator 96 | # 97 | # @return [Enumerator] The enumerator of the reserved ports list. 98 | def reserved_ports 99 | Enumerator.new do 100 | all_ports = @conn.get("resources/ports") do |r| 101 | r.options.context = { 102 | orka_auth_type: :token, 103 | } 104 | end.body["reserved_ports"] 105 | 106 | node_ports = all_ports.select do |hash| 107 | hash["orka_node_name"] == @name 108 | end 109 | 110 | node_ports.map do |mapping| 111 | ProtocolPortMapping.new( 112 | host_port: mapping["host_port"], 113 | guest_port: mapping["guest_port"], 114 | protocol: mapping["protocol"], 115 | ) 116 | end 117 | end 118 | end 119 | 120 | # Tag this node as sandbox. This limits deployment management from the Orka CLI. You can perform only 121 | # Kubernetes deployment management with +kubectl+, {https://helm.sh/docs/helm/#helm Helm}, and Tiller. 122 | # 123 | # @macro auth_token_and_license 124 | # 125 | # @note This request is supported for Intel nodes only. 126 | # 127 | # @return [void] 128 | def enable_sandbox 129 | body = { 130 | orka_node_name: @name, 131 | }.compact 132 | @conn.post("resources/node/sandbox", body) do |r| 133 | r.options.context = { 134 | orka_auth_type: [:license, :token], 135 | } 136 | end 137 | end 138 | 139 | # Remove the sandbox tag from this node. This re-enables deployment management with the Orka CLI. 140 | # 141 | # @macro auth_token_and_license 142 | # 143 | # @note This request is supported for Intel nodes only. 144 | # 145 | # @return [void] 146 | def disable_sandbox 147 | @conn.delete("resources/node/sandbox") do |r| 148 | r.body = { 149 | orka_node_name: @name, 150 | }.compact 151 | 152 | r.options.context = { 153 | orka_auth_type: [:license, :token], 154 | } 155 | end 156 | end 157 | 158 | # Dedicate this node to a specified user group. Only users from this user group will be able to deploy to the 159 | # node. 160 | # 161 | # @macro auth_token_and_license 162 | # 163 | # @param [String, nil] group The user group to dedicate the node to. 164 | # @return [void] 165 | def dedicate_to_group(group) 166 | body = [@name].compact 167 | @conn.post("resources/node/groups/#{group || "$ungrouped"}", body) do |r| 168 | r.options.context = { 169 | orka_auth_type: [:license, :token], 170 | } 171 | end 172 | @orka_group = group 173 | end 174 | 175 | # Make this node available to all users. 176 | # 177 | # @macro auth_token_and_license 178 | # 179 | # @return [void] 180 | def remove_group_dedication 181 | dedicate_to_group(nil) 182 | end 183 | 184 | # Assign the specified tag to the specified node (enable node affinity). When node affinity is configured, 185 | # Orka first attempts to deploy to the specified node or group of nodes, before moving to any other nodes. 186 | # 187 | # @macro auth_token_and_license 188 | # 189 | # @param [String] tag_name The name of the tag. 190 | # @return [void] 191 | def tag(tag_name) 192 | body = { 193 | orka_node_name: @name, 194 | }.compact 195 | @conn.post("resources/node/tag/#{tag_name}", body) do |r| 196 | r.options.context = { 197 | orka_auth_type: [:license, :token], 198 | } 199 | end 200 | @tags << tag_name 201 | end 202 | 203 | # Remove the specified tag from the specified node. 204 | # 205 | # @macro auth_token_and_license 206 | # 207 | # @param [String] tag_name The name of the tag. 208 | # @return [void] 209 | def untag(tag_name) 210 | @conn.delete("resources/node/tag/#{tag_name}") do |r| 211 | r.body = { 212 | orka_node_name: @name, 213 | }.compact 214 | 215 | r.options.context = { 216 | orka_auth_type: [:license, :token], 217 | } 218 | end 219 | @tags.delete(tag_name) 220 | end 221 | 222 | private 223 | 224 | def lazy_initialize 225 | # We don't use /resources/node/status/{name} as it only provides partial data. 226 | url = "resources/node/list" 227 | url += "/all" if @admin 228 | response = @conn.get(url) do |r| 229 | auth_type = [:token] 230 | auth_type << :license if @admin 231 | r.options.context = { 232 | orka_auth_type: auth_type, 233 | } 234 | end 235 | node = response.body["nodes"].find { |hash| hash["name"] == @name } 236 | 237 | raise ResourceNotFoundError, "No node found matching \"#{@name}\"." if node.nil? 238 | 239 | deserialize(node) 240 | super 241 | end 242 | 243 | def deserialize(hash) 244 | @name = hash["name"] 245 | @host_name = hash["host_name"] 246 | @address = hash["address"] 247 | @host_ip = hash["hostIP"] 248 | @available_cpu_cores = hash["available_cpu"] 249 | @allocatable_cpu_cores = hash["allocatable_cpu"] 250 | @available_gpu_count = if hash["available_gpu"] == "N/A" 251 | 0 252 | else 253 | hash["available_gpu"].to_i 254 | end 255 | @allocatable_gpu_count = if hash["allocatable_gpu"] == "N/A" 256 | 0 257 | else 258 | hash["allocatable_gpu"].to_i 259 | end 260 | @available_memory = hash["available_memory"] 261 | @total_cpu_cores = hash["total_cpu"] 262 | @total_memory = hash["total_memory"] 263 | @type = hash["node_type"] 264 | @state = hash["state"] 265 | @orka_group = hash["orka_group"] 266 | @tags = hash["orka_tags"] 267 | end 268 | end 269 | end 270 | end 271 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/vm_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | 5 | module OrkaAPI 6 | module Models 7 | # A template configuration (a container template) consisting of a 8 | # {https://orkadocs.macstadium.com/docs/orka-glossary#base-image base image}, a 9 | # {https://orkadocs.macstadium.com/docs/orka-glossary#snapshot-image snapshot image}, and the number of CPU cores 10 | # to be used. To become a VM that you can run in the cloud, a VM configuration needs to be deployed to a {Node 11 | # node}. 12 | # 13 | # You can deploy multiple VMs from a single VM configuration. Once created, you can no longer modify a VM 14 | # configuration. 15 | # 16 | # Deleting a VM does not delete the VM configuration it was deployed from. 17 | class VMConfiguration < LazyModel 18 | # @return [String] The name of this VM configuration. 19 | attr_reader :name 20 | 21 | # @return [User] The owner of this VM configuration, i.e. the user which deployed it. 22 | lazy_attr :owner 23 | 24 | # @return [Image] The base image which newly deployed VMs of this configuration will boot from. 25 | lazy_attr :base_image 26 | 27 | # @return [Integer] The number of CPU cores to allocate to deployed VMs of this configuration. 28 | lazy_attr :cpu_cores 29 | 30 | # @return [Integer] The number of VCPUs to allocate to deployed VMs of this configuration. 31 | lazy_attr :vcpu_count 32 | 33 | # @return [ISO] The ISO to attach to deployed VMs of this configuration. 34 | lazy_attr :iso_image 35 | 36 | # @return [Image] The storage disk image to attach to deployed VMs of this configuration. 37 | lazy_attr :attached_disk 38 | 39 | # @return [Boolean] True if the VNC console should be enabled for deployed VMs of this configuration. 40 | lazy_attr :vnc_console? 41 | 42 | # @return [Boolean] True if IO boost should be enabled for deployed VMs of this configuration. 43 | lazy_attr :io_boost? 44 | 45 | # @return [Boolean] True if network boost should be enabled for deployed VMs of this configuration. 46 | lazy_attr :net_boost? 47 | 48 | # @return [Boolean] True if deployed VMs of this configuration should use a prior saved state (created via 49 | # {VMInstance#save_state}) rather than a clean base image. 50 | lazy_attr :use_saved_state? 51 | 52 | # @return [Boolean] True if GPU passthrough should be enabled for deployed VMs of this configuration. 53 | lazy_attr :gpu_passthrough? 54 | 55 | # @return [String, nil] The custom system serial number, if set. 56 | lazy_attr :system_serial 57 | 58 | # @return [String, nil] The tag that VMs of this configuration should be deployed to, if any. 59 | lazy_attr :tag 60 | 61 | # @return [Boolean] Whether it is mandatory that VMs are deployed to the requested tag. 62 | lazy_attr :tag_required? 63 | 64 | # @return [Symbol] The scheduler mode chosen for VM deployment. Can be either +:default+ or +:most_allocated+. 65 | lazy_attr :scheduler 66 | 67 | # @return [Numeric, nil] The amount of RAM this VM is assigned to take, in gigabytes. If not set, Orka will 68 | # automatically select a value when deploying. 69 | lazy_attr :memory 70 | 71 | # @api private 72 | # @param [String] name 73 | # @param [Connection] conn 74 | # @return [VMConfiguration] 75 | def self.lazy_prepare(name:, conn:) 76 | new(conn: conn, name: name) 77 | end 78 | 79 | # @api private 80 | # @param [Hash] hash 81 | # @param [Connection] conn 82 | # @return [VMConfiguration] 83 | def self.from_hash(hash, conn:) 84 | new(conn: conn, hash: hash) 85 | end 86 | 87 | # @private 88 | # @param [Connection] conn 89 | # @param [String] name 90 | # @param [Hash] hash 91 | def initialize(conn:, name: nil, hash: nil) 92 | super(!hash.nil?) 93 | @conn = conn 94 | @name = name 95 | deserialize(hash) if hash 96 | end 97 | 98 | # Deploy the VM configuration to a node. If you don't specify a node, Orka chooses a node based on the 99 | # available resources. 100 | # 101 | # @macro auth_token 102 | # 103 | # @param [Node, String] node The node on which to deploy the VM. The node must have sufficient CPU and memory 104 | # to accommodate the VM. 105 | # @param [Integer] replicas The scale at which to deploy the VM configuration. If not specified, defaults to 106 | # +1+ (non-scaled). The option is supported for VMs deployed on Intel nodes only. 107 | # @param [Array] reserved_ports One or more port mappings that forward traffic to the specified 108 | # ports on the VM. The following ports and port ranges are reserved and cannot be used: +22+, +443+, +6443+, 109 | # +5000-5014+, +5999-6013+, +8822-8836+. 110 | # @param [Boolean] iso_install Set to +true+ if you want to use an ISO. The option is supported for VMs 111 | # deployed on Intel nodes only. 112 | # @param [Models::ISO, String] iso_image An ISO to attach to the VM during deployment. If already set in the 113 | # respective VM configuration and not set here, Orka applies the setting from the VM configuration. You can 114 | # also use this field to override any ISO specified in the VM configuration. The option is supported for VMs 115 | # deployed on Intel nodes only. 116 | # @param [Boolean] attach_disk Set to +true+ if you want to attach additional storage during deployment. The 117 | # option is supported for VMs deployed on Intel nodes only. 118 | # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM during 119 | # deployment. If already set in the respective VM configuration and not set here, Orka applies the setting 120 | # from the VM configuration. You can also use this field to override any storage specified in the VM 121 | # configuration. The option is supported for VMs deployed on Intel nodes only. 122 | # @param [Boolean] vnc_console Enables or disables VNC for the VM. If not set in the VM configuration or here, 123 | # defaults to +true+. If already set in the respective VM configuration and not set here, Orka applies the 124 | # setting from the VM configuration. You can also use this field to override the VNC setting specified in the 125 | # VM configuration. 126 | # @param [Hash{String => String}] vm_metadata Inject custom metadata to the VM. If not set, only the built-in 127 | # metadata is injected into the VM. 128 | # @param [String] system_serial Assign an owned macOS system serial number to the VM. If already set in the 129 | # respective VM configuration and not set here, Orka applies the setting from the VM configuration. The 130 | # option is supported for VMs deployed on Intel nodes only. 131 | # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. If not set in the VM 132 | # configuration or here, defaults to +false+. If already set in the respective VM configuration and not set 133 | # here, Orka applies the setting from the VM configuration. You can also use this field to override the GPU 134 | # passthrough setting specified in the VM configuration. When enabled, +vnc_console+ is automatically 135 | # disabled. The option is supported for VMs deployed on Intel nodes only. GPU passthrough must first be 136 | # enabled in your cluster. 137 | # @param [String] tag When specified, the VM is preferred to be deployed to a node marked with this tag. 138 | # @param [Boolean] tag_required By default, +false+. When set to +true+, the VM is required to be deployed to a 139 | # node marked with this tag. 140 | # @param [Symbol] scheduler Possible values are +:default+ and +:most-allocated+. By default, +:default+. When 141 | # set to +:most-allocated+ the deployed VM will be scheduled to nodes having most of their resources 142 | # allocated. +:default+ keeps used vs free resources balanced between the nodes. 143 | # @return [VMDeploymentResult] Details of the just-deployed VM. 144 | def deploy(node: nil, replicas: nil, reserved_ports: nil, iso_install: nil, 145 | iso_image: nil, attach_disk: nil, attached_disk: nil, vnc_console: nil, 146 | vm_metadata: nil, system_serial: nil, gpu_passthrough: nil, 147 | tag: nil, tag_required: nil, scheduler: nil) 148 | VMResource.lazy_prepare(name: @name, conn: @conn).deploy( 149 | node: node, 150 | replicas: replicas, 151 | reserved_ports: reserved_ports, 152 | iso_install: iso_install, 153 | iso_image: iso_image, 154 | attach_disk: attach_disk, 155 | attached_disk: attached_disk, 156 | vnc_console: vnc_console, 157 | vm_metadata: vm_metadata, 158 | system_serial: system_serial, 159 | gpu_passthrough: gpu_passthrough, 160 | tag: tag, 161 | tag_required: tag_required, 162 | scheduler: scheduler.to_s, 163 | ) 164 | end 165 | 166 | # Remove the VM configuration and all VM deployments of it. 167 | # 168 | # If the VM configuration and its deployments belong to the user associated with the client's token then the 169 | # client only needs to be configured with a token. Otherwise, if you are removing a VM resource associated with 170 | # another user, you need to configure the client with both a token and a license key. 171 | # 172 | # @return [void] 173 | def purge 174 | VMResource.lazy_prepare(name: @name, conn: @conn).purge 175 | end 176 | 177 | # Delete the VM configuration state. Now when you deploy the VM configuration it will use the base image to 178 | # boot the VM. 179 | # 180 | # To delete a VM state, it must not be used by any deployed VM. 181 | # 182 | # @macro auth_token 183 | # 184 | # @note This request is supported for VMs deployed on Intel nodes only. 185 | # 186 | # @return [void] 187 | def delete_saved_state 188 | @conn.delete("resources/vm/configs/#{@name}/delete-state") do |r| 189 | r.options.context = { 190 | orka_auth_type: :token, 191 | } 192 | end 193 | end 194 | 195 | private 196 | 197 | def lazy_initialize 198 | response = @conn.get("resources/vm/configs/#{@name}") do |r| 199 | r.options.context = { 200 | orka_auth_type: :token, 201 | } 202 | end 203 | configs = response.body["configs"] 204 | 205 | raise ResourceNotFoundError, "No VM configuration found matching \"#{@name}\"." if configs.empty? 206 | 207 | deserialize(configs.first) 208 | super 209 | end 210 | 211 | def deserialize(hash) 212 | @name = hash["orka_vm_name"] 213 | @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) 214 | @base_image = Image.lazy_prepare(name: hash["orka_base_image"], conn: @conn) 215 | @cpu_cores = hash["orka_cpu_core"] 216 | @vcpu_count = hash["vcpu_count"] 217 | @iso_image = if hash["iso_image"] == "None" 218 | nil 219 | else 220 | ISO.lazy_prepare(name: hash["iso_image"], conn: @conn) 221 | end 222 | @attached_disk = if hash["attached_disk"] == "None" 223 | nil 224 | else 225 | Image.lazy_prepare(name: hash["attached_disk"], conn: @conn) 226 | end 227 | @vnc_console = hash["vnc_console"] 228 | @io_boost = hash["io_boost"] 229 | @net_boost = hash["net_boost"] 230 | @use_saved_state = hash["use_saved_state"] 231 | @gpu_passthrough = hash["gpu_passthrough"] 232 | @system_serial = if hash["system_serial"] == "N/A" 233 | nil 234 | else 235 | hash["system_serial"] 236 | end 237 | @tag = if hash["tag"].empty? 238 | nil 239 | else 240 | hash["tag"] 241 | end 242 | @tag_required = hash["tag_required"] 243 | @scheduler = if hash["scheduler"].nil? 244 | :default 245 | else 246 | hash["scheduler"].tr("-", "_").to_sym 247 | end 248 | @memory = if hash["memory"] == "automatic" 249 | nil 250 | else 251 | hash["memory"] 252 | end 253 | end 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/vm_instance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "attr_predicate" 4 | require_relative "protocol_port_mapping" 5 | require_relative "disk" 6 | 7 | module OrkaAPI 8 | module Models 9 | # A virtual machine deployed on a {Node node} from an existing {VMConfiguration VM configuration} or cloned from 10 | # an existing virtual machine. In the case of macOS VMs, this is a full macOS VM inside of a 11 | # {https://www.docker.com/resources/what-container Docker container}. 12 | class VMInstance 13 | extend AttrPredicate 14 | 15 | # @return [String] The ID of the VM. 16 | attr_reader :id 17 | 18 | # @return [String] The name of the VM. 19 | attr_reader :name 20 | 21 | # @return [Node] The node the VM is deployed on. 22 | attr_reader :node 23 | 24 | # @return [User] The owner of the VM, i.e. the user which deployed it. 25 | attr_reader :owner 26 | 27 | # @return [String] The state of the node the VM is deployed on. 28 | attr_reader :node_status 29 | 30 | # @return [String] The IP of the VM. 31 | attr_reader :ip 32 | 33 | # @return [Integer] The port used to connect to the VM via VNC. 34 | attr_reader :vnc_port 35 | 36 | # @return [Integer] The port used to connect to the VM via macOS Screen Sharing. 37 | attr_reader :screen_sharing_port 38 | 39 | # @return [Integer] The port used to connect to the VM via SSH. 40 | attr_reader :ssh_port 41 | 42 | # @return [Integer] The number of CPU cores allocated to the VM. 43 | attr_reader :cpu_cores 44 | 45 | # @return [Integer] The number of vCPUs allocated to the VM. 46 | attr_reader :vcpu_count 47 | 48 | # @return [Integer] The number of GPUs allocated to the VM. 49 | attr_reader :gpu_count 50 | 51 | # @return [String] The amount of RAM allocated to the VM. 52 | attr_reader :ram 53 | 54 | # @return [Image] The base image the VM was deployed from. 55 | attr_reader :base_image 56 | 57 | # @return [VMConfiguration] The VM configuration object this instance is based on. 58 | attr_reader :config 59 | 60 | # @return [String] 61 | attr_reader :configuration_template 62 | 63 | # @return [String] The status of the VM, at the time this class was initialized. 64 | attr_reader :status 65 | 66 | # @return [Boolean] True if IO boost is enabled for this VM. 67 | attr_predicate :io_boost 68 | 69 | # @return [Boolean] True if network boost is enabled for this VM. 70 | attr_predicate :net_boost 71 | 72 | # @return [Boolean] True if this VM is using a prior saved state rather than a clean base image. 73 | attr_predicate :use_saved_state 74 | 75 | # @return [Array] The port mappings established for this VM. 76 | attr_reader :reserved_ports 77 | 78 | # @return [DateTime] The time when this VM was deployed. 79 | attr_reader :creation_time 80 | 81 | # @return [String, nil] The tag that was requested this VM be deployed to, if any. 82 | attr_reader :tag 83 | 84 | # @return [Boolean] Whether it was mandatory that this VM was deployed to the requested tag. 85 | attr_predicate :tag_required 86 | 87 | # @api private 88 | # @param [Hash] hash 89 | # @param [Connection] conn 90 | # @param [Boolean] admin 91 | def initialize(hash, conn:, admin: false) 92 | @conn = conn 93 | @admin = admin 94 | deserialize(hash) 95 | end 96 | 97 | # @!macro [new] vm_instance_state_note 98 | # @note Calling this will not change the state of this object, and thus not change the return values of 99 | # attributes like {#status}. You must fetch a new object instance from the client to refresh this data. 100 | 101 | # Remove the VM instance. 102 | # 103 | # If the VM instance belongs to the user associated with the client's token then the client only needs to be 104 | # configured with a token. Otherwise, if you are removing a VM instance associated with another user, you need 105 | # to configure the client with both a token and a license key. 106 | # 107 | # @macro vm_instance_state_note 108 | # 109 | # @return [void] 110 | def delete 111 | @conn.delete("resources/vm/delete") do |r| 112 | r.body = { 113 | orka_vm_name: @id, 114 | }.compact 115 | 116 | auth_type = [:token] 117 | auth_type << :license if @admin 118 | r.options.context = { 119 | orka_auth_type: auth_type, 120 | } 121 | end 122 | end 123 | 124 | # Power ON the VM. 125 | # 126 | # @macro auth_token 127 | # 128 | # @macro vm_instance_state_note 129 | # 130 | # @note This request is supported for VMs deployed on Intel nodes only. 131 | # 132 | # @return [void] 133 | def start 134 | body = { 135 | orka_vm_name: @id, 136 | }.compact 137 | @conn.post("resources/vm/exec/start", body) do |r| 138 | r.options.context = { 139 | orka_auth_type: :token, 140 | } 141 | end 142 | end 143 | 144 | # Power OFF the VM. 145 | # 146 | # @macro auth_token 147 | # 148 | # @macro vm_instance_state_note 149 | # 150 | # @note This request is supported for VMs deployed on Intel nodes only. 151 | # 152 | # @return [void] 153 | def stop 154 | body = { 155 | orka_vm_name: @id, 156 | }.compact 157 | @conn.post("resources/vm/exec/stop", body) do |r| 158 | r.options.context = { 159 | orka_auth_type: :token, 160 | } 161 | end 162 | end 163 | 164 | # Suspend the VM. 165 | # 166 | # @macro auth_token 167 | # 168 | # @macro vm_instance_state_note 169 | # 170 | # @note This request is supported for VMs deployed on Intel nodes only. 171 | # 172 | # @return [void] 173 | def suspend 174 | body = { 175 | orka_vm_name: @id, 176 | }.compact 177 | @conn.post("resources/vm/exec/suspend", body) do |r| 178 | r.options.context = { 179 | orka_auth_type: :token, 180 | } 181 | end 182 | end 183 | 184 | # Resume the VM. The VM must already be suspended. 185 | # 186 | # @macro auth_token 187 | # 188 | # @macro vm_instance_state_note 189 | # 190 | # @note This request is supported for VMs deployed on Intel nodes only. 191 | # 192 | # @return [void] 193 | def resume 194 | body = { 195 | orka_vm_name: @id, 196 | }.compact 197 | @conn.post("resources/vm/exec/resume", body) do |r| 198 | r.options.context = { 199 | orka_auth_type: :token, 200 | } 201 | end 202 | end 203 | 204 | # Revert the VM to the latest state of its base image. This operation restarts the VM. 205 | # 206 | # @macro auth_token 207 | # 208 | # @macro vm_instance_state_note 209 | # 210 | # @note This request is supported for VMs deployed on Intel nodes only. 211 | # 212 | # @return [void] 213 | def revert 214 | body = { 215 | orka_vm_name: @id, 216 | }.compact 217 | @conn.post("resources/vm/exec/revert", body) do |r| 218 | r.options.context = { 219 | orka_auth_type: :token, 220 | } 221 | end 222 | end 223 | 224 | # List the disks attached to the VM. The VM must be non-scaled. 225 | # 226 | # @macro auth_token 227 | # 228 | # @macro lazy_enumerator 229 | # 230 | # @note This request is supported for VMs deployed on Intel nodes only. 231 | # 232 | # @return [Enumerator] The enumerator of the disk list. 233 | def disks 234 | Enumerator.new do 235 | drives = @conn.get("resources/vm/list-disks") do |r| 236 | r.options.context = { 237 | orka_auth_type: :token, 238 | } 239 | end.body["drives"] 240 | drives.map do |hash| 241 | Disk.new( 242 | type: hash["type"], 243 | device: hash["device"], 244 | target: hash["target"], 245 | source: hash["source"], 246 | ) 247 | end 248 | end 249 | end 250 | 251 | # Attach a disk to the VM. The VM must be non-scaled. 252 | # 253 | # You can attach any of the following disks: 254 | # 255 | # * Any disks created with {Client#generate_empty_image} 256 | # * Any non-bootable images available in your Orka storage and listed by {Client#images} 257 | # 258 | # @macro auth_token 259 | # 260 | # @note Before you can use the attached disk, you need to restart the VM with a {#stop manual stop} of the VM, 261 | # followed by a {#start manual start} VM. A software reboot from the OS will not trigger macOS to recognize 262 | # the disk. 263 | # 264 | # @note This request is supported for VMs deployed on Intel nodes only. 265 | # 266 | # @param [Image, String] image The disk to attach to the VM. 267 | # @param [String] mount_point The mount point to attach the VM to. 268 | # @return [void] 269 | def attach_disk(image:, mount_point:) 270 | body = { 271 | orka_vm_name: @id, 272 | image_name: image.is_a?(Image) ? image.name : image, 273 | mount_point: mount_point, 274 | }.compact 275 | @conn.post("resources/vm/attach-disk", body) do |r| 276 | r.options.context = { 277 | orka_auth_type: :token, 278 | } 279 | end 280 | end 281 | 282 | # Save the VM configuration state (disk and memory). 283 | # 284 | # If VM state is previously saved, it is overwritten. To overwrite the VM state, it must not be used by any 285 | # deployed VM. 286 | # 287 | # @macro auth_token 288 | # 289 | # @note Saving VM state is restricted only to VMs that have GPU passthrough disabled. 290 | # 291 | # @note This request is supported for VMs deployed on Intel nodes only. 292 | # 293 | # @return [void] 294 | def save_state 295 | body = { 296 | orka_vm_name: @id, 297 | }.compact 298 | @conn.post("resources/vm/configs/save-state", body) do |r| 299 | r.options.context = { 300 | orka_auth_type: :token, 301 | } 302 | end 303 | end 304 | 305 | # Apply the current state of the VM's image to the original base image in the Orka storage. Use this operation 306 | # to modify an existing base image. All VM configs that reference this base image will be affected. 307 | # 308 | # The VM must be non-scaled. The base image to which you want to commit changes must be in use by only one VM. 309 | # The base image to which you want to commit changes must not be in use by a VM configuration with saved VM 310 | # state. 311 | # 312 | # @macro auth_token 313 | # 314 | # @return [void] 315 | def commit_to_base_image 316 | body = { 317 | orka_vm_name: @id, 318 | }.compact 319 | @conn.post("resources/image/commit", body) do |r| 320 | r.options.context = { 321 | orka_auth_type: :token, 322 | } 323 | end 324 | end 325 | 326 | # Save the current state of the VM's image to a new base image in the Orka storage. Use this operation to 327 | # create a new base image. 328 | # 329 | # The VM must be non-scaled. The base image name that you specify must not be in use. 330 | # 331 | # @macro auth_token 332 | # 333 | # @param [String] image_name The name to give to the new base image. 334 | # @return [Image] The lazily-loaded new base image. 335 | def save_new_base_image(image_name) 336 | body = { 337 | orka_vm_name: @id, 338 | new_name: image_name, 339 | }.compact 340 | @conn.post("resources/image/save", body) do |r| 341 | r.options.context = { 342 | orka_auth_type: :token, 343 | } 344 | end 345 | Image.lazy_prepare(image_name, conn: @conn) 346 | end 347 | 348 | # Resize the current disk of the VM and save it as a new base image. This does not affect the original base 349 | # image of the VM. 350 | # 351 | # @macro auth_token 352 | # 353 | # @param [String] username The username of the VM user. 354 | # @param [String] password The password of the VM user. 355 | # @param [String] image_name The new name for the resized image. 356 | # @param [String] image_size The size of the new image (in k, M, G, or T), for example +"100G"+. 357 | # @return [Image] The lazily-loaded new base image. 358 | def resize_image(username:, password:, image_name:, image_size:) 359 | body = { 360 | orka_vm_name: @id, 361 | vm_username: username, 362 | vm_password: password, 363 | new_image_size: image_size, 364 | new_image_name: image_name, 365 | }.compact 366 | @conn.post("resources/image/resize", body) do |r| 367 | r.options.context = { 368 | orka_auth_type: :token, 369 | } 370 | end 371 | Image.lazy_prepare(image_name, conn: @conn) 372 | end 373 | 374 | private 375 | 376 | def deserialize(hash) 377 | @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) 378 | @name = hash["virtual_machine_name"] 379 | @id = hash["virtual_machine_id"] 380 | @node = Node.lazy_prepare(name: hash["node_location"], conn: @conn, admin: @admin) 381 | @node_status = hash["node_status"] 382 | @ip = hash["virtual_machine_ip"] 383 | @vnc_port = hash["vnc_port"].to_i 384 | @screen_sharing_port = hash["screen_sharing_port"].to_i 385 | @ssh_port = hash["ssh_port"].to_i 386 | @cpu_cores = hash["cpu"] 387 | @vcpu_count = hash["vcpu"] 388 | @gpu_count = if hash["gpu"] == "N/A" 389 | 0 390 | else 391 | hash["gpu"].to_i 392 | end 393 | @ram = hash["RAM"] 394 | @base_image = Image.lazy_prepare(name: hash["base_image"], conn: @conn) 395 | @config = VMConfiguration.lazy_prepare(name: hash["image"], conn: @conn) 396 | @configuration_template = hash["configuration_template"] 397 | @status = hash["vm_status"] 398 | @io_boost = hash["io_boost"] 399 | @net_boost = if hash["net_boost"] == "N/A" 400 | true # arm64 401 | else 402 | hash["net_boost"] 403 | end 404 | @use_saved_state = hash["use_saved_state"] 405 | @reserved_ports = hash["reserved_ports"].map do |mapping| 406 | ProtocolPortMapping.new( 407 | host_port: mapping["host_port"], 408 | guest_port: mapping["guest_port"], 409 | protocol: mapping["protocol"], 410 | ) 411 | end 412 | @creation_time = DateTime.iso8601(hash["creation_timestamp"]) 413 | @tag = if hash["tag"].nil? || hash["tag"].empty? 414 | nil 415 | else 416 | hash["tag"] 417 | end 418 | @tag_required = !!hash["tag_required"] 419 | # Replicas count also passed. Should always be 1 since we expand those. 420 | end 421 | end 422 | end 423 | end 424 | -------------------------------------------------------------------------------- /lib/orka_api_client/models/vm_resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lazy_model" 4 | require_relative "vm_instance" 5 | require_relative "vm_deployment_result" 6 | 7 | module OrkaAPI 8 | module Models 9 | # A general representation of {VMConfiguration VM configurations} and the {VMInstance VMs} deployed from those 10 | # configurations. 11 | class VMResource < LazyModel 12 | # @return [String] The name of this VM resource. 13 | attr_reader :name 14 | 15 | # @return [Boolean] True if there are associated deployed VM instances. 16 | lazy_attr :deployed? 17 | 18 | # @return [Array] The list of deployed VM instances. 19 | lazy_attr :instances 20 | 21 | # @return [User, nil] The owner of the associated VM configuration. This is +nil+ if {#deployed?} is +true+. 22 | lazy_attr :owner 23 | 24 | # @return [Integer, nil] The number of CPU cores to use, specified by the associated VM configuration. This is 25 | # +nil+ if {#deployed?} is +true+. 26 | lazy_attr :cpu_cores 27 | 28 | # @return [Integer, nil] The number of vCPUs to use, specified by the associated VM configuration. This is 29 | # +nil+ if {#deployed?} is +true+. 30 | lazy_attr :vcpu_count 31 | 32 | # @return [Image, nil] The base image to use, specified by the associated VM configuration. This is +nil+ if 33 | # {#deployed?} is +true+. 34 | lazy_attr :base_image 35 | 36 | # @return [VMConfiguration, nil] The matching VM configuration object. This is +nil+ if {#deployed?} is +true+. 37 | lazy_attr :config 38 | 39 | # @return [Boolean, nil] True if IO boost is enabled, specified by the associated VM configuration. This is 40 | # +nil+ if {#deployed?} is +true+. 41 | lazy_attr :io_boost? 42 | 43 | # @return [Boolean, nil] True if network boost is enabled, specified by the associated VM configuration. This is 44 | # +nil+ if {#deployed?} is +true+. 45 | lazy_attr :net_boost? 46 | 47 | # @return [Boolean, nil] True if the saved state should be used rather than cleanly from the base image, 48 | # specified by the associated VM configuration. This is +nil+ if {#deployed?} is +true+. 49 | lazy_attr :use_saved_state? 50 | 51 | # @return [Boolean, nil] True if GPU passthrough is enabled, specified by the associated VM configuration. This 52 | # is +nil+ if {#deployed?} is +true+. 53 | lazy_attr :gpu_passthrough? 54 | 55 | # @return [String, nil] 56 | lazy_attr :configuration_template 57 | 58 | # @return [String, nil] The amount of RAM assigned for this VM, if it has been manually configured in advance of 59 | # deployment. This is always +nil+ if {#deployed?} is +true+. 60 | lazy_attr :ram 61 | 62 | # @api private 63 | # @param [String] name 64 | # @param [Connection] conn 65 | # @param [Boolean] admin 66 | # @return [VMResource] 67 | def self.lazy_prepare(name:, conn:, admin: false) 68 | new(conn: conn, name: name, admin: admin) 69 | end 70 | 71 | # @api private 72 | # @param [Hash] hash 73 | # @param [Connection] conn 74 | # @param [Boolean] admin 75 | # @return [VMResource] 76 | def self.from_hash(hash, conn:, admin: false) 77 | new(conn: conn, hash: hash, admin: admin) 78 | end 79 | 80 | # @private 81 | # @param [Connection] conn 82 | # @param [String] name 83 | # @param [Hash] hash 84 | # @param [Boolean] admin 85 | def initialize(conn:, name: nil, hash: nil, admin: false) 86 | super(!hash.nil?) 87 | @conn = conn 88 | @name = name 89 | @admin = admin 90 | deserialize(hash) if hash 91 | end 92 | 93 | # @!macro [new] vm_resource_state_note 94 | # @note Calling this will not change the state of this object, and thus not change the return values of 95 | # {#deployed?} and {#instances}, if the object already been loaded. You must fetch a new object instance or 96 | # call {#refresh} to refresh this data. 97 | 98 | # Deploy an existing VM configuration to a node. If you don't specify a node, Orka chooses a node based on the 99 | # available resources. 100 | # 101 | # @macro auth_token 102 | # 103 | # @macro vm_resource_state_note 104 | # 105 | # @param [Node, String] node The node on which to deploy the VM. The node must have sufficient CPU and memory 106 | # to accommodate the VM. 107 | # @param [Integer] replicas The scale at which to deploy the VM configuration. If not specified, defaults to 108 | # +1+ (non-scaled). The option is supported for VMs deployed on Intel nodes only. 109 | # @param [Array] reserved_ports One or more port mappings that forward traffic to the specified 110 | # ports on the VM. The following ports and port ranges are reserved and cannot be used: +22+, +443+, +6443+, 111 | # +5000-5014+, +5999-6013+, +8822-8836+. 112 | # @param [Boolean] iso_install Set to +true+ if you want to use an ISO. The option is supported for VMs 113 | # deployed on Intel nodes only. 114 | # @param [Models::ISO, String] iso_image An ISO to attach to the VM during deployment. If already set in the 115 | # respective VM configuration and not set here, Orka applies the setting from the VM configuration. You can 116 | # also use this field to override any ISO specified in the VM configuration. The option is supported for VMs 117 | # deployed on Intel nodes only. 118 | # @param [Boolean] attach_disk Set to +true+ if you want to attach additional storage during deployment. The 119 | # option is supported for VMs deployed on Intel nodes only. 120 | # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM during 121 | # deployment. If already set in the respective VM configuration and not set here, Orka applies the setting 122 | # from the VM configuration. You can also use this field to override any storage specified in the VM 123 | # configuration. The option is supported for VMs deployed on Intel nodes only. 124 | # @param [Boolean] vnc_console Enables or disables VNC for the VM. If not set in the VM configuration or here, 125 | # defaults to +true+. If already set in the respective VM configuration and not set here, Orka applies the 126 | # setting from the VM configuration. You can also use this field to override the VNC setting specified in the 127 | # VM configuration. 128 | # @param [Hash{String => String}] vm_metadata Inject custom metadata to the VM. If not set, only the built-in 129 | # metadata is injected into the VM. 130 | # @param [String] system_serial Assign an owned macOS system serial number to the VM. If already set in the 131 | # respective VM configuration and not set here, Orka applies the setting from the VM configuration. The 132 | # option is supported for VMs deployed on Intel nodes only. 133 | # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. If not set in the VM 134 | # configuration or here, defaults to +false+. If already set in the respective VM configuration and not set 135 | # here, Orka applies the setting from the VM configuration. You can also use this field to override the GPU 136 | # passthrough setting specified in the VM configuration. When enabled, +vnc_console+ is automatically 137 | # disabled. The option is supported for VMs deployed on Intel nodes only. GPU passthrough must first be 138 | # enabled in your cluster. 139 | # @param [String] tag When specified, the VM is preferred to be deployed to a node marked with this tag. 140 | # @param [Boolean] tag_required By default, +false+. When set to +true+, the VM is required to be deployed to a 141 | # node marked with this tag. 142 | # @param [Symbol] scheduler Possible values are +:default+ and +:most-allocated+. By default, +:default+. When 143 | # set to +:most-allocated+ the deployed VM will be scheduled to nodes having most of their resources 144 | # allocated. +:default+ keeps used vs free resources balanced between the nodes. 145 | # @return [VMDeploymentResult] Details of the just-deployed VM. 146 | def deploy(node: nil, replicas: nil, reserved_ports: nil, iso_install: nil, 147 | iso_image: nil, attach_disk: nil, attached_disk: nil, vnc_console: nil, 148 | vm_metadata: nil, system_serial: nil, gpu_passthrough: nil, 149 | tag: nil, tag_required: nil, scheduler: nil) 150 | vm_metadata = { items: vm_metadata.map { |k, v| { key: k, value: v } } } unless vm_metadata.nil? 151 | body = { 152 | orka_vm_name: @name, 153 | orka_node_name: node.is_a?(Node) ? node.name : node, 154 | replicas: replicas, 155 | reserved_ports: reserved_ports&.map { |mapping| "#{mapping.host_port}:#{mapping.guest_port}" }, 156 | iso_install: iso_install, 157 | iso_image: iso_image.is_a?(ISO) ? iso_image.name : iso_image, 158 | attach_disk: attach_disk, 159 | attached_disk: attached_disk.is_a?(Image) ? attached_disk.name : attached_disk, 160 | vnc_console: vnc_console, 161 | vm_metadata: vm_metadata, 162 | system_serial: system_serial, 163 | gpu_passthrough: gpu_passthrough, 164 | tag: tag, 165 | tag_required: tag_required, 166 | scheduler: scheduler.to_s, 167 | }.compact 168 | response = @conn.post("resources/vm/deploy", body) do |r| 169 | r.options.context = { 170 | orka_auth_type: :token, 171 | } 172 | end 173 | VMDeploymentResult.new(response.body, conn: @conn, admin: @admin) 174 | end 175 | 176 | # Removes all VM instances. 177 | # 178 | # If the VM instances belongs to the user associated with the client's token then the client only needs to be 179 | # configured with a token. Otherwise, if you are removing VM instances associated with another user, you need 180 | # to configure the client with both a token and a license key. 181 | # 182 | # @macro vm_resource_state_note 183 | # 184 | # @param [Node, String] node If specified, only remove VM deployments on that node. 185 | # @return [void] 186 | def delete_all_instances(node: nil) 187 | @conn.delete("resources/vm/delete") do |r| 188 | r.body = { 189 | orka_vm_name: @name, 190 | orka_node_name: node.is_a?(Node) ? node.name : node, 191 | }.compact 192 | 193 | auth_type = [:token] 194 | auth_type << :license if @admin 195 | r.options.context = { 196 | orka_auth_type: auth_type, 197 | } 198 | end 199 | rescue Faraday::ServerError => e 200 | no_deployments = [ 201 | "No VMs with that name are currently deployed", 202 | "No deployments exists for this VM name", 203 | ] 204 | raise if no_deployments.none? { |msg| e.response[:body]&.include?(msg) } 205 | end 206 | 207 | # Remove all VM instances and the VM configuration. 208 | # 209 | # If the VM resource belongs to the user associated with the client's token then the client only needs to be 210 | # configured with a token. Otherwise, if you are removing a VM resource associated with another user, you need 211 | # to configure the client with both a token and a license key. 212 | # 213 | # @macro vm_resource_state_note 214 | # 215 | # @return [void] 216 | def purge 217 | @conn.delete("resources/vm/purge") do |r| 218 | r.body = { 219 | orka_vm_name: @name, 220 | }.compact 221 | 222 | auth_type = [:token] 223 | auth_type << :license if @admin 224 | r.options.context = { 225 | orka_auth_type: auth_type, 226 | } 227 | end 228 | end 229 | 230 | # Power ON all VM instances on a particular node that are associated with this VM resource. 231 | # 232 | # @macro auth_token 233 | # 234 | # @macro vm_resource_state_note 235 | # 236 | # @note This request is supported for VMs deployed on Intel nodes only. 237 | # 238 | # @param [Node, String] node All deployments of this VM located on this node will be started. 239 | # @return [void] 240 | def start_all_on_node(node) 241 | raise ArgumentError, "Node cannot be nil." if node.nil? 242 | 243 | body = { 244 | orka_vm_name: @name, 245 | orka_node_name: node.is_a?(Node) ? node.name : node, 246 | }.compact 247 | @conn.post("resources/vm/exec/start", body) do |r| 248 | r.options.context = { 249 | orka_auth_type: :token, 250 | } 251 | end 252 | end 253 | 254 | # Power OFF all VM instances on a particular node that are associated with this VM resource. 255 | # 256 | # @macro auth_token 257 | # 258 | # @macro vm_resource_state_note 259 | # 260 | # @note This request is supported for VMs deployed on Intel nodes only. 261 | # 262 | # @param [Node, String] node All deployments of this VM located on this node will be stopped. 263 | # @return [void] 264 | def stop_all_on_node(node) 265 | raise ArgumentError, "Node cannot be nil." if node.nil? 266 | 267 | body = { 268 | orka_vm_name: @name, 269 | orka_node_name: node.is_a?(Node) ? node.name : node, 270 | }.compact 271 | @conn.post("resources/vm/exec/stop", body) do |r| 272 | r.options.context = { 273 | orka_auth_type: :token, 274 | } 275 | end 276 | end 277 | 278 | # Suspend all VM instances on a particular node that are associated with this VM resource. 279 | # 280 | # @macro auth_token 281 | # 282 | # @macro vm_resource_state_note 283 | # 284 | # @note This request is supported for VMs deployed on Intel nodes only. 285 | # 286 | # @param [Node, String] node All deployments of this VM located on this node will be suspended. 287 | # @return [void] 288 | def suspend_all_on_node(node) 289 | raise ArgumentError, "Node cannot be nil." if node.nil? 290 | 291 | body = { 292 | orka_vm_name: @name, 293 | orka_node_name: node.is_a?(Node) ? node.name : node, 294 | }.compact 295 | @conn.post("resources/vm/exec/suspend", body) do |r| 296 | r.options.context = { 297 | orka_auth_type: :token, 298 | } 299 | end 300 | end 301 | 302 | # Resume all VM instances on a particular node that are associated with this VM resource. 303 | # 304 | # @macro auth_token 305 | # 306 | # @macro vm_resource_state_note 307 | # 308 | # @note This request is supported for VMs deployed on Intel nodes only. 309 | # 310 | # @param [Node, String] node All deployments of this VM located on this node will be resumed. 311 | # @return [void] 312 | def resume_all_on_node(node) 313 | raise ArgumentError, "Node cannot be nil." if node.nil? 314 | 315 | body = { 316 | orka_vm_name: @name, 317 | orka_node_name: node.is_a?(Node) ? node.name : node, 318 | }.compact 319 | @conn.post("resources/vm/exec/resume", body) do |r| 320 | r.options.context = { 321 | orka_auth_type: :token, 322 | } 323 | end 324 | end 325 | 326 | # Revert all VM instances on a particular node that are associated with this VM resource to the latest state of 327 | # its base image. This operation restarts the VMs. 328 | # 329 | # @macro auth_token 330 | # 331 | # @macro vm_resource_state_note 332 | # 333 | # @note This request is supported for VMs deployed on Intel nodes only. 334 | # 335 | # @param [Node, String] node All deployments of this VM located on this node will be reverted. 336 | # @return [void] 337 | def revert_all_on_node(node) 338 | raise ArgumentError, "Node cannot be nil." if node.nil? 339 | 340 | body = { 341 | orka_vm_name: @name, 342 | orka_node_name: node.is_a?(Node) ? node.name : node, 343 | }.compact 344 | @conn.post("resources/vm/exec/revert", body) do |r| 345 | r.options.context = { 346 | orka_auth_type: :token, 347 | } 348 | end 349 | end 350 | 351 | private 352 | 353 | def lazy_initialize 354 | response = @conn.get("resources/vm/status/#{@name}") do |r| 355 | auth_type = [:token] 356 | auth_type << :license if @admin 357 | r.options.context = { 358 | orka_auth_type: auth_type, 359 | } 360 | end 361 | resources = response.body["virtual_machine_resources"] 362 | 363 | raise ResourceNotFoundError, "No VM resource found matching \"#{@name}\"." if resources.empty? 364 | 365 | deserialize(resources.first) 366 | super 367 | end 368 | 369 | def deserialize(hash) 370 | @name = hash["virtual_machine_name"] 371 | @deployed = case hash["vm_deployment_status"] 372 | when "Deployed" 373 | true 374 | when "Not Deployed" 375 | false 376 | else 377 | raise UnrecognisedStateError, "Unrecognised VM deployment status." 378 | end 379 | 380 | if @deployed 381 | @instances = hash["status"].map { |instance| VMInstance.new(instance, conn: @conn, admin: @admin) } 382 | else 383 | @instances = [] 384 | @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) 385 | @cpu_cores = hash["cpu"] 386 | @vcpu_count = hash["vcpu"] 387 | @base_image = Image.lazy_prepare(name: hash["base_image"], conn: @conn) 388 | @config = VMConfiguration.lazy_prepare(name: hash["image"], conn: @conn) 389 | @io_boost = hash["io_boost"] 390 | @net_boost = hash["net_boost"] 391 | @use_saved_state = hash["use_saved_state"] 392 | @gpu_passthrough = hash["gpu_passthrough"] 393 | @configuration_template = hash["configuration_template"] 394 | @ram = hash["RAM"] 395 | end 396 | end 397 | end 398 | end 399 | end 400 | -------------------------------------------------------------------------------- /lib/orka_api_client/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "errors" 4 | require_relative "connection" 5 | require_relative "models/enumerator" 6 | require_relative "models/user" 7 | require_relative "models/vm_configuration" 8 | require_relative "models/vm_resource" 9 | require_relative "models/node" 10 | require_relative "models/image" 11 | require_relative "models/remote_image" 12 | require_relative "models/iso" 13 | require_relative "models/remote_iso" 14 | require_relative "models/kube_account" 15 | require_relative "models/token_info" 16 | require_relative "models/password_requirements" 17 | 18 | module OrkaAPI 19 | # This is the entrypoint class for all interactions with the Orka API. 20 | class Client 21 | # Creates an instance of the client for a given Orka service endpoint and associated credentials. 22 | # 23 | # @param [String] base_url The API URL for the Orka service endpoint. 24 | # @param [String] token The token used for authentication. This can be generated with {#create_token} from an 25 | # credentialless client. 26 | # @param [String] license_key The Orka license key used for authentication in administrative operations. 27 | def initialize(base_url, token: nil, license_key: nil) 28 | @conn = Connection.new(base_url, token: token, license_key: license_key) 29 | @license_key = license_key 30 | end 31 | 32 | # @!macro [new] lazy_enumerator 33 | # The network operation is not performed immediately upon return of this method. The request is performed when 34 | # any action is performed on the enumerator, or otherwise forced via {Models::Enumerator#eager}. 35 | 36 | # @!macro [new] lazy_object 37 | # The network operation is not performed immediately upon return of this method. The request is performed when 38 | # any attribute is accessed or any method is called on the returned object, or otherwise forced via 39 | # {Models::LazyModel#eager}. Successful return from this method does not guarantee the requested resource 40 | # exists. 41 | 42 | # @!macro [new] auth_none 43 | # This method does not require the client to be configured with any credentials. 44 | 45 | # @!macro [new] auth_token 46 | # This method requires the client to be configured with a token. 47 | 48 | # @!macro [new] auth_license 49 | # This method requires the client to be configured with a license key. 50 | 51 | # @!macro [new] auth_token_and_license 52 | # This method requires the client to be configured with both a token and a license key. 53 | 54 | # @!group Users 55 | 56 | # Retrieve a list of the users in the Orka environment. 57 | # 58 | # @macro auth_license 59 | # 60 | # @macro lazy_enumerator 61 | # 62 | # @return [Models::Enumerator] The enumerator of the user list. 63 | def users 64 | Models::Enumerator.new do 65 | users = [] 66 | body = @conn.get("users") do |r| 67 | r.options.context = { 68 | orka_auth_type: :license, 69 | } 70 | end.body 71 | groups = body["user_groups"] 72 | if groups.nil? 73 | user_list = body["user_list"] 74 | user_list.each do |user| 75 | users << Models::User.new( 76 | conn: @conn, 77 | email: user, 78 | group: "$ungrouped", 79 | ) 80 | end 81 | else 82 | groups.each do |group, group_users| 83 | group_users.each do |group_user| 84 | users << Models::User.new( 85 | conn: @conn, 86 | email: group_user, 87 | group: group, 88 | ) 89 | end 90 | end 91 | end 92 | users 93 | end 94 | end 95 | 96 | # Fetches information on a particular user in the Orka environment. 97 | # 98 | # @macro auth_license 99 | # 100 | # @macro lazy_object 101 | # 102 | # @param [String] email The email of the user to fetch. 103 | # @return [Models::User] The lazily-loaded user object. 104 | def user(email) 105 | Models::User.lazy_prepare(email: email, conn: @conn) 106 | end 107 | 108 | # Create a new user in the Orka environment. You need to specify email address and password. You cannot pass an 109 | # email address that's already in use. 110 | # 111 | # @macro auth_license 112 | # 113 | # @param [String] email An email address for the user. This also serves as the username. 114 | # @param [String] password A password for the user. Must be at least 6 characters long. 115 | # @param [String] group A user group for the user. Once set, you can no longer change the user group. 116 | # @return [Models::User] The user object. 117 | def create_user(email:, password:, group: nil) 118 | body = { 119 | email: email, 120 | password: password, 121 | group: group, 122 | }.compact 123 | @conn.post("users", body) do |r| 124 | r.options.context = { 125 | orka_auth_type: :license, 126 | } 127 | end 128 | 129 | group = "$ungrouped" if group.nil? 130 | Models::User.new(conn: @conn, email: email, group: group) 131 | end 132 | 133 | # Modify the email address or password of the current user. This operation is intended for regular Orka users. 134 | # 135 | # @macro auth_token 136 | # 137 | # @param [String] email The new email address for the user. 138 | # @param [String] password The new password for the user. 139 | # @return [void] 140 | def update_user_credentials(email: nil, password: nil) 141 | raise ArgumentError, "Must update either the username or password, or both." if email.nil? && password.nil? 142 | 143 | body = { 144 | email: email, 145 | password: password, 146 | }.compact 147 | @conn.put("users", body) do |r| 148 | r.options.context = { 149 | orka_auth_type: :token, 150 | } 151 | end 152 | end 153 | 154 | # @!endgroup 155 | # @!group Tokens 156 | 157 | # Create an authentication token using an existing user's email and password. 158 | # 159 | # @macro auth_none 160 | # 161 | # @param [Models::User, String] user The user or their associated email address. 162 | # @param [String] password The user's password. 163 | # @return [String] The authentication token. 164 | def create_token(user:, password:) 165 | body = { 166 | email: user_email(user), 167 | password: password, 168 | }.compact 169 | @conn.post("token", body).body["token"] 170 | end 171 | 172 | # Revoke the token associated with this client instance. 173 | # 174 | # @macro auth_token 175 | # 176 | # @return [void] 177 | def revoke_token 178 | @conn.delete("token") do |r| 179 | r.options.context = { 180 | orka_auth_type: :token, 181 | } 182 | end 183 | end 184 | 185 | # @!endgroup 186 | # @!group VMs 187 | 188 | # Retrieve a list of the VMs and VM configurations. By default this fetches resources associated with the 189 | # client's token, but you can optionally request a list of resources for another user (or all users). 190 | # 191 | # If you filter by a user, or request all users, this method requires the client to be configured with both a 192 | # token and a license key. Otherwise, it only requires a token. 193 | # 194 | # @macro lazy_enumerator 195 | # 196 | # @param [Models::User, String] user The user, or their associated email address, to use instead of the one 197 | # associated with the client's token. Pass "all" if you wish to fetch for all users. 198 | # @return [Models::Enumerator] The enumerator of the VM resource list. 199 | def vm_resources(user: nil) 200 | Models::Enumerator.new do 201 | url = "resources/vm/list" 202 | url += "/#{user}" unless user.nil? 203 | resources = @conn.get(url, { expand: nil }) do |r| 204 | auth_type = [:token] 205 | auth_type << :license unless user.nil? 206 | r.options.context = { 207 | orka_auth_type: auth_type, 208 | } 209 | end.body["virtual_machine_resources"] 210 | resources.map { |hash| Models::VMResource.from_hash(hash, conn: @conn, admin: !user.nil?) } 211 | end 212 | end 213 | 214 | # Fetches information on a particular VM or VM configuration. 215 | # 216 | # If you set the admin parameter to true, this method requires the client to be configured with both a 217 | # token and a license key. Otherwise, it only requires a token. 218 | # 219 | # @macro lazy_object 220 | # 221 | # @param [String] name The name of the VM resource to fetch. 222 | # @param [Boolean] admin Set to true to allow VM resources associated with other users to be queried. 223 | # @return [Models::VMResource] The lazily-loaded VM resource object. 224 | def vm_resource(name, admin: false) 225 | Models::VMResource.lazy_prepare(name: name, conn: @conn, admin: admin) 226 | end 227 | 228 | # Retrieve a list of the VM configurations associated with the client's token. Orka returns information about the 229 | # base image, CPU cores, owner and name of the VM configurations. 230 | # 231 | # @macro auth_token 232 | # 233 | # @macro lazy_enumerator 234 | # 235 | # @return [Models::Enumerator] The enumerator of the VM configuration list. 236 | def vm_configurations 237 | Models::Enumerator.new do 238 | configs = @conn.get("resources/vm/configs") do |r| 239 | r.options.context = { 240 | orka_auth_type: :token, 241 | } 242 | end.body["configs"] 243 | configs.map { |hash| Models::VMConfiguration.from_hash(hash, conn: @conn) } 244 | end 245 | end 246 | 247 | # Fetches information on a particular VM configuration. 248 | # 249 | # @macro auth_token 250 | # 251 | # @macro lazy_object 252 | # 253 | # @param [String] name The name of the VM configuration to fetch. 254 | # @return [Models::VMConfiguration] The lazily-loaded VM configuration. 255 | def vm_configuration(name) 256 | Models::VMConfiguration.lazy_prepare(name: name, conn: @conn) 257 | end 258 | 259 | # Create a VM configuration that is ready for deployment. In Orka, VM configurations are container templates. 260 | # You can deploy multiple VMs from a single VM configuration. You cannot modify VM configurations. 261 | # 262 | # @macro auth_token 263 | # 264 | # @param [String] name The name of the VM configuration. This string must consist of lowercase Latin alphanumeric 265 | # characters or the dash (+-+). This string must begin and end with an alphanumeric character. This string must 266 | # not exceed 38 characters. 267 | # @param [Models::Image, String] base_image The name of the base image that you want to use with the 268 | # configuration. If you want to attach an ISO to the VM configuration from which to install macOS, make sure 269 | # that the base image is an empty disk of a sufficient size. 270 | # @param [Models::Image, String] snapshot_image A name for the 271 | # {https://orkadocs.macstadium.com/docs/orka-glossary#section-snapshot-image snapshot image} of the VM. 272 | # Typically, the same value as +name+. 273 | # @param [Integer] cpu_cores The number of CPU cores to dedicate for the VM. Must be 3, 4, 6, 8, 12, or 24. 274 | # @param [Integer] vcpu_count The number of vCPUs for the VM. Must equal the number of CPUs, when CPU is less 275 | # than or equal to 3. Otherwise, must equal half of or exactly the number of CPUs specified. 276 | # @param [Models::ISO, String] iso_image An ISO to attach to the VM on deployment. The option is supported for 277 | # VMs deployed on Intel nodes only. 278 | # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM on deployment. The 279 | # option is supported for VMs deployed on Intel nodes only. 280 | # @param [Boolean] vnc_console By default, +true+. Enables or disables VNC for the VM configuration. You can 281 | # override on deployment of specific VMs. 282 | # @param [String] system_serial Assign an owned macOS system serial number to the VM configuration. The option is 283 | # supported for VMs deployed on Intel nodes only. 284 | # @param [Boolean] io_boost By default, +false+ for VM configurations created before Orka 1.5. Default value for 285 | # VM configurations created with Orka 1.5 or later depends on the cluster default. Enables or disables IO 286 | # performance improvements for the VM configuration. The option is supported for VMs deployed on Intel nodes 287 | # only. 288 | # @param [Boolean] net_boost By default, +false+ for VM configurations created before Orka 2.3.0. Default value 289 | # for VM configurations created with Orka 2.3.0 or later depends on the cluster default. Enables or disables 290 | # network performance improvements for the VM configuration. The option is supported for VMs deployed on Intel 291 | # nodes only. 292 | # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. When enabled, +vnc_console+ is 293 | # automatically disabled. The option is supported for VMs deployed on Intel nodes only. GPU passthrough must 294 | # first be enabled in your cluster. 295 | # @param [String] tag When specified, the VM is preferred to be deployed to a node marked with this tag. 296 | # @param [Boolean] tag_required By default, +false+. When set to +true+, the VM is required to be deployed to a 297 | # node marked with this tag. 298 | # @param [Symbol] scheduler Possible values are +:default+ and +:most-allocated+. By default, +:default+. When 299 | # set to +:most-allocated+ VMs deployed from the VM configuration will be scheduled to nodes having most of 300 | # their resources allocated. +:default+ keeps used vs free resources balanced between the nodes. 301 | # @param [Numeric] memory 302 | # @return [Models::VMConfiguration] The lazily-loaded VM configuration. 303 | def create_vm_configuration(name, 304 | base_image:, snapshot_image:, cpu_cores:, vcpu_count:, 305 | iso_image: nil, attached_disk: nil, vnc_console: nil, 306 | system_serial: nil, io_boost: nil, net_boost: nil, gpu_passthrough: nil, 307 | tag: nil, tag_required: nil, scheduler: nil, memory: nil) 308 | body = { 309 | orka_vm_name: name, 310 | orka_base_image: base_image.is_a?(Models::Image) ? base_image.name : base_image, 311 | orka_image: snapshot_image.is_a?(Models::Image) ? snapshot_image.name : snapshot_image, 312 | orka_cpu_core: cpu_cores, 313 | vcpu_count: vcpu_count, 314 | iso_image: iso_image.is_a?(Models::ISO) ? iso_image.name : iso_image, 315 | attached_disk: attached_disk.is_a?(Models::Image) ? attached_disk.name : attached_disk, 316 | vnc_console: vnc_console, 317 | system_serial: system_serial, 318 | io_boost: io_boost, 319 | net_boost: net_boost, 320 | gpu_passthrough: gpu_passthrough, 321 | tag: tag, 322 | tag_required: tag_required, 323 | scheduler: scheduler.to_s, 324 | memory: memory, 325 | }.compact 326 | @conn.post("resources/vm/create", body) do |r| 327 | r.options.context = { 328 | orka_auth_type: :token, 329 | } 330 | end 331 | vm_configuration(name) 332 | end 333 | 334 | # @!endgroup 335 | # @!group Nodes 336 | 337 | # Retrieve a list of the nodes in your Orka environment. Orka returns a list of nodes with IP and resource 338 | # information. 339 | # 340 | # If you set the admin parameter to true, this method requires the client to be configured with both a 341 | # token and a license key. Otherwise, it only requires a token. 342 | # 343 | # @macro lazy_enumerator 344 | # 345 | # @param [Boolean] admin Set to true to allow nodes dedicated to other users to be queried. 346 | # @return [Models::Enumerator] The enumerator of the node list. 347 | def nodes(admin: false) 348 | Models::Enumerator.new do 349 | url = "resources/node/list" 350 | url += "/all" if admin 351 | nodes = @conn.get(url) do |r| 352 | auth_type = [:token] 353 | auth_type << :license if admin 354 | r.options.context = { 355 | orka_auth_type: auth_type, 356 | } 357 | end.body["nodes"] 358 | nodes.map { |hash| Models::Node.from_hash(hash, conn: @conn, admin: admin) } 359 | end 360 | end 361 | 362 | # Fetches information on a particular node. 363 | # 364 | # If you set the admin parameter to true, this method requires the client to be configured with both a 365 | # token and a license key. Otherwise, it only requires a token. 366 | # 367 | # @macro lazy_object 368 | # 369 | # @param [String] name The name of the node to fetch. 370 | # @param [Boolean] admin Set to true to allow nodes dedicated with other users to be queried. 371 | # @return [Models::VMResource] The lazily-loaded node object. 372 | def node(name, admin: false) 373 | Models::Node.lazy_prepare(name: name, conn: @conn, admin: admin) 374 | end 375 | 376 | # @!endgroup 377 | # @!group Images 378 | 379 | # Retrieve a list of the base images and empty disks in your Orka environment. 380 | # 381 | # @macro auth_token 382 | # 383 | # @macro lazy_enumerator 384 | # 385 | # @return [Models::Enumerator] The enumerator of the image list. 386 | def images 387 | Models::Enumerator.new do 388 | images = @conn.get("resources/image/list") do |r| 389 | r.options.context = { 390 | orka_auth_type: :token, 391 | } 392 | end.body["image_attributes"] 393 | images.map { |hash| Models::Image.from_hash(hash, conn: @conn) } 394 | end 395 | end 396 | 397 | # Fetches information on a particular image. 398 | # 399 | # @macro auth_token 400 | # 401 | # @macro lazy_object 402 | # 403 | # @param [String] name The name of the image to fetch. 404 | # @return [Models::Image] The lazily-loaded image. 405 | def image(name) 406 | Models::Image.lazy_prepare(name: name, conn: @conn) 407 | end 408 | 409 | # List the base images available in the Orka remote repo. 410 | # 411 | # To use one of the images from the remote repo, you can {Models::RemoteImage#pull pull} it into the local Orka 412 | # storage. 413 | # 414 | # @macro auth_token 415 | # 416 | # @macro lazy_enumerator 417 | # 418 | # @return [Models::Enumerator] The enumerator of the remote image list. 419 | def remote_images 420 | Models::Enumerator.new do 421 | images = @conn.get("resources/image/list-remote") do |r| 422 | r.options.context = { 423 | orka_auth_type: :token, 424 | } 425 | end.body["images"] 426 | images.map { |name| Models::RemoteImage.new(name, conn: @conn) } 427 | end 428 | end 429 | 430 | # Returns an object representing a remote image of a specified name. 431 | # 432 | # Note that this method does not perform any network requests and does not verify if the name supplied actually 433 | # exists in the Orka remote repo. 434 | # 435 | # @param [String] name The name of the remote image. 436 | # @return [Models::RemoteImage] The remote image object. 437 | def remote_image(name) 438 | Models::RemoteImage.new(name, conn: @conn) 439 | end 440 | 441 | # Generate an empty base image. You can use it to create VM configurations that will use an ISO or you can attach 442 | # it to a deployed VM to extend its storage. 443 | # 444 | # @macro auth_token 445 | # 446 | # @note This request is supported for Intel images only. Intel images have +.img+ extension. 447 | # 448 | # @param [String] name The name of this new image. 449 | # @param [String] size The size of this new image (in K, M, G, or T), for example +"10G"+. 450 | # @return [Models::Image] The new lazily-loaded image. 451 | def generate_empty_image(name, size:) 452 | body = { 453 | file_name: name, 454 | file_size: size, 455 | }.compact 456 | @conn.post("resources/image/generate", body) do |r| 457 | r.options.context = { 458 | orka_auth_type: :token, 459 | } 460 | end 461 | image(name) 462 | end 463 | 464 | # Upload an image to the Orka environment. 465 | # 466 | # @macro auth_token 467 | # 468 | # @note This request is supported for Intel images only. Intel images have +.img+ extension. 469 | # 470 | # @param [String, IO] file The string file path or an open IO object to the image to upload. 471 | # @param [String] name The name to give to this image. Defaults to the local filename. 472 | # @return [Models::Image] The new lazily-loaded image. 473 | def upload_image(file, name: nil) 474 | file_part = Faraday::Multipart::FilePart.new( 475 | file, 476 | "application/x-iso9660-image", 477 | name, 478 | ) 479 | body = { image: file_part } 480 | @conn.post("resources/image/upload", body, "Content-Type" => "multipart/form-data") do |r| 481 | r.options.context = { 482 | orka_auth_type: :token, 483 | } 484 | end 485 | image(file_part.original_filename) 486 | end 487 | 488 | # @!endgroup 489 | # @!group ISOs 490 | 491 | # Retrieve a list of the ISOs available in the local Orka storage. 492 | # 493 | # @macro auth_token 494 | # 495 | # @macro lazy_enumerator 496 | # 497 | # @note All ISO requests are supported for Intel nodes only. 498 | # 499 | # @return [Models::Enumerator] The enumerator of the ISO list. 500 | def isos 501 | Models::Enumerator.new do 502 | isos = @conn.get("resources/iso/list") do |r| 503 | r.options.context = { 504 | orka_auth_type: :token, 505 | } 506 | end.body["iso_attributes"] 507 | isos.map { |hash| Models::ISO.from_hash(hash, conn: @conn) } 508 | end 509 | end 510 | 511 | # Fetches information on a particular ISO in local Orka storage. 512 | # 513 | # @macro auth_token 514 | # 515 | # @macro lazy_object 516 | # 517 | # @note All ISO requests are supported for Intel nodes only. 518 | # 519 | # @param [String] name The name of the ISO to fetch. 520 | # @return [Models::ISO] The lazily-loaded ISO. 521 | def iso(name) 522 | Models::ISO.lazy_prepare(name: name, conn: @conn) 523 | end 524 | 525 | # List the ISOs available in the Orka remote repo. 526 | # 527 | # To use one of the ISOs from the remote repo, you can {Models::RemoteISO#pull pull} it into the local Orka 528 | # storage. 529 | # 530 | # @macro auth_token 531 | # 532 | # @macro lazy_enumerator 533 | # 534 | # @note All ISO requests are supported for Intel nodes only. 535 | # 536 | # @return [Models::Enumerator] The enumerator of the remote ISO list. 537 | def remote_isos 538 | Models::Enumerator.new do 539 | isos = @conn.get("resources/iso/list-remote") do |r| 540 | r.options.context = { 541 | orka_auth_type: :token, 542 | } 543 | end.body["isos"] 544 | isos.map { |name| Models::RemoteISO.new(name, conn: @conn) } 545 | end 546 | end 547 | 548 | # Returns an object representing a remote ISO of a specified name. 549 | # 550 | # Note that this method does not perform any network requests and does not verify if the name supplied actually 551 | # exists in the Orka remote repo. 552 | # 553 | # @note All ISO requests are supported for Intel nodes only. 554 | # 555 | # @param [String] name The name of the remote ISO. 556 | # @return [Models::RemoteISO] The remote ISO object. 557 | def remote_iso(name) 558 | Models::RemoteISO.new(name, conn: @conn) 559 | end 560 | 561 | # Upload an ISO to the Orka environment. 562 | # 563 | # @macro auth_token 564 | # 565 | # @note All ISO requests are supported for Intel nodes only. 566 | # 567 | # @param [String, IO] file The string file path or an open IO object to the ISO to upload. 568 | # @param [String] name The name to give to this ISO. Defaults to the local filename. 569 | # @return [Models::ISO] The new lazily-loaded ISO. 570 | def upload_iso(file, name: nil) 571 | file_part = Faraday::Multipart::FilePart.new( 572 | file, 573 | "application/x-iso9660-image", 574 | name, 575 | ) 576 | body = { iso: file_part } 577 | @conn.post("resources/iso/upload", body, "Content-Type" => "multipart/form-data") do |r| 578 | r.options.context = { 579 | orka_auth_type: :token, 580 | } 581 | end 582 | iso(file_part.original_filename) 583 | end 584 | 585 | # @!endgroup 586 | # @!group Kube-Accounts 587 | 588 | # Retrieve a list of kube-accounts associated with an Orka user. 589 | # 590 | # @macro auth_token_and_license 591 | # 592 | # @macro lazy_enumerator 593 | # 594 | # @param [Models::User, String] user The user, which can be specified by the user object or their email address, 595 | # for which we are returning the associated kube-accounts of. Defaults to the user associated with the client's 596 | # token. 597 | # @return [Models::Enumerator] The enumerator of the kube-account list. 598 | def kube_accounts(user: nil) 599 | Models::Enumerator.new do 600 | accounts = @conn.get("resources/kube-account") do |r| 601 | email = user_email(user) 602 | r.body = { 603 | email: email, 604 | }.compact 605 | 606 | r.options.context = { 607 | orka_auth_type: [:token, :license], 608 | } 609 | end.body["serviceAccounts"] 610 | accounts.map { |name| Models::KubeAccount.new(name, email: email, conn: @conn) } 611 | end 612 | end 613 | 614 | # Returns an object representing a kube-account of a particular user. 615 | # 616 | # Note that this method does not perform any network requests and does not verify if the name supplied actually 617 | # exists in the Orka environment. 618 | # 619 | # @param [String] name The name of the kube-account. 620 | # @param [Models::User, String] user The user, which can be specified by the user object or their email address, 621 | # of which the kube-account is associated with. Defaults to the user associated with the client's token. 622 | # @return [Models::KubeAccount] The kube-account object. 623 | def kube_account(name, user: nil) 624 | Models::KubeAccount.new(name, email: user_email(user), conn: @conn) 625 | end 626 | 627 | # Create a kube-account. 628 | # 629 | # @macro auth_token_and_license 630 | # 631 | # @param [String] name The name of the kube-account. 632 | # @param [Models::User, String] user The user, which can be specified by the user object or their email address, 633 | # of which the kube-account will be associated with. Defaults to the user associated with the client's token. 634 | # @return [Models::KubeAccount] The created kube-account. 635 | def create_kube_account(name, user: nil) 636 | email = user_email(user) 637 | body = { 638 | name: name, 639 | email: email, 640 | }.compact 641 | kubeconfig = @conn.post("resources/kube-account", body) do |r| 642 | r.options.context = { 643 | orka_auth_type: [:token, :license], 644 | } 645 | end.body["kubeconfig"] 646 | Models::KubeAccount.new(name, email: email, kubeconfig: kubeconfig, conn: @conn) 647 | end 648 | 649 | # Delete all kube-accounts associated with a user. 650 | # 651 | # @macro auth_token_and_license 652 | # 653 | # @param [Models::User, String] user The user, which can be specified by the user object or their email address, 654 | # which will have their associated kube-account deleted. Defaults to the user associated with the client's 655 | # token. 656 | # @return [void] 657 | def delete_all_kube_accounts(user: nil) 658 | @conn.delete("resources/kube-account") do |r| 659 | email = user_email(user) 660 | r.body = { 661 | email: email, 662 | }.compact 663 | 664 | r.options.context = { 665 | orka_auth_type: [:token, :license], 666 | } 667 | end 668 | end 669 | 670 | # @!endgroup 671 | # @!group Logs 672 | 673 | # Retrieve a log of all CLI commands and API requests executed against your Orka environment. 674 | # 675 | # @macro auth_license 676 | # 677 | # @param [Integer] limit Limit the amount of results returned to this quantity. 678 | # @param [DateTime] start Limit the results to be log entries after this date. 679 | # @param [String] query The LogQL query to filter by. Defaults to +{log_type="user_logs"}+. 680 | # @return [Hash] A raw Grafana Loki query result payload. Parsing this is out-of-scope for this gem. 681 | def logs(limit: nil, start: nil, query: nil) 682 | @conn.post("logs/query") do |r| 683 | r.params[:logs20] = "true" 684 | r.params[:limit] = limit unless limit.nil? 685 | r.params[:start] = start.rfc3339 unless start.nil? 686 | r.params[:query] = query unless query.nil? 687 | 688 | r.options.context = { 689 | orka_auth_type: :license, 690 | } 691 | end.body 692 | end 693 | 694 | # @!endgroup 695 | # @!group Environment Checks 696 | 697 | # Retrieve information about the token associated with the client. The request returns information about the 698 | # associated email address, the authentication status of the token, and if the token is revoked. 699 | # 700 | # @macro auth_token 701 | # 702 | # @return [Models::TokenInfo] Information about the token. 703 | def token_info 704 | body = @conn.get("token") do |r| 705 | r.options.context = { 706 | orka_auth_type: :token, 707 | } 708 | end.body 709 | Models::TokenInfo.new(body, conn: @conn) 710 | end 711 | 712 | # Retrieve detailed information about the health of your Orka environment. 713 | # 714 | # @macro auth_none 715 | # 716 | # @return [Hash] The status information on different components of your environment. 717 | def environment_status 718 | @conn.get("health-check").body["status"] 719 | end 720 | 721 | # Retrieve the current API version of your Orka environment. 722 | # 723 | # @macro auth_none 724 | # 725 | # @return [String] The remote API version. 726 | def remote_api_version 727 | @conn.get("version").body["api_version"] 728 | end 729 | 730 | # Retrieve the current version of the components in your Orka environment. 731 | # 732 | # @macro auth_none 733 | # 734 | # @return [Hash{String => String}] The version of each component. 735 | def environment_component_versions 736 | @conn.get("version", { all: "true" }).body.filter_map do |key, value| 737 | new_key = key.delete_suffix("_version") 738 | [new_key, value] if new_key != key 739 | end.to_h 740 | end 741 | 742 | # Retrieve the current password requirements for creating an Orka user. 743 | # 744 | # @macro auth_none 745 | # 746 | # @return [Models::PasswordRequirements] The password requirements. 747 | def password_requirements 748 | Models::PasswordRequirements.new(@conn.get("validation-requirements").body) 749 | end 750 | 751 | # Check if a license key is authorized or not. 752 | # 753 | # @macro auth_none 754 | # 755 | # @param [String] license_key The license key to check. Defaults to the one associated with the client. 756 | # @return [Boolean] True if the license key is valid. 757 | def license_key_valid?(license_key = @license_key) 758 | raise ArgumentError, "License key is required." if license_key.nil? 759 | 760 | @conn.get("validate-license-key") do |r| 761 | r.body = { 762 | licenseKey: license_key, 763 | } 764 | end 765 | true 766 | rescue Faraday::UnauthorizedError 767 | false 768 | end 769 | 770 | # Retrieve the default base image for the Orka environment. 771 | # 772 | # @macro auth_none 773 | # 774 | # @return [Models::Image] The lazily-loaded default base image object. 775 | def default_base_image 776 | Models::Image.lazy_prepare(name: @conn.get("default-base-image").body["default_base_image"], conn: @conn) 777 | end 778 | 779 | # Upload a custom TLS certificate and its private key in PEM format from your computer to your cluster. You can 780 | # then access Orka via {https://orkadocs.macstadium.com/docs/custom-tls-certificate external custom domain}. 781 | # 782 | # The certificate and the key must meet the following requirements: 783 | # 784 | # * Both files are in PEM format. 785 | # * The private key is not passphrase protected. 786 | # * The certificate might be any of the following: 787 | # * A single domain certificate (e.g. +company.com+). 788 | # * Multi-domain certificate (e.g. +app1.company.com+, +app2.company.com+, and so on). 789 | # * Wildcard TLS certificate (e.g. +*.company.com+). If containing an asterisk, it must be a single asterisk 790 | # and must be in the leftmost position of the domain name. For example: You cannot use a +*.*.company.com+ 791 | # certificate to work with Orka. 792 | # * A certificate chain (bundle) that contains your server, intermediates, and root certificates concatenated 793 | # (in the proper order) into one file. 794 | # * The certificate must be a domain certificate issued by a certificate authority for a registered domain OR a 795 | # self-signed certificate for any domain 796 | # ({https://orkadocs.macstadium.com/docs/custom-tls-certificate#32-create-a-local-mapping for local use only}). 797 | # 798 | # @macro auth_token_and_license 799 | # 800 | # @param [String, IO] cert The string file path or an open IO object to the certificate. 801 | # @param [String, IO] key The string file path or an open IO object to the key. 802 | # @return [void] 803 | def upload_tls_certificate(cert, key) 804 | cert_part = Faraday::Multipart::FilePart.new(cert, "application/x-pem-file") 805 | key_part = Faraday::Multipart::FilePart.new(key, "application/x-pem-file") 806 | body = { certPath: cert_part, keyPath: key_part } 807 | @conn.post("resources/cert/set", body, "Content-Type" => "multipart/form-data") do |r| 808 | r.options.context = { 809 | orka_auth_type: [:token, :license], 810 | } 811 | end 812 | end 813 | 814 | # @!endgroup 815 | 816 | alias inspect to_s 817 | 818 | private 819 | 820 | def user_email(user) 821 | if user.is_a?(Models::User) 822 | user.email 823 | else 824 | user 825 | end 826 | end 827 | end 828 | end 829 | --------------------------------------------------------------------------------