From 938d658d897077e9d923d2f4d1d7af2b0c6b1240 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Wed, 26 Jan 2022 16:06:05 +0000 Subject: [PATCH] Initial commit --- .editorconfig | 9 + .github/workflows/main.yml | 37 + .gitignore | 12 + .parlour | 12 + .rspec | 3 + .rubocop.yml | 76 ++ .yardopts | 5 + Gemfile | 17 + Gemfile.lock | 101 +++ README.md | 41 + Rakefile | 22 + bin/console | 15 + bin/setup | 8 + lib/orka_api_client.rb | 4 + lib/orka_api_client/auth_middleware.rb | 40 + lib/orka_api_client/client.rb | 739 ++++++++++++++++++ lib/orka_api_client/connection.rb | 30 + lib/orka_api_client/errors.rb | 16 + lib/orka_api_client/models/attr_predicate.rb | 22 + lib/orka_api_client/models/disk.rb | 32 + lib/orka_api_client/models/enumerator.rb | 29 + lib/orka_api_client/models/image.rb | 179 +++++ lib/orka_api_client/models/iso.rb | 130 +++ lib/orka_api_client/models/kube_account.rb | 63 ++ lib/orka_api_client/models/lazy_model.rb | 52 ++ lib/orka_api_client/models/log_entry.rb | 99 +++ lib/orka_api_client/models/node.rb | 208 +++++ .../models/password_requirements.rb | 17 + .../models/protocol_port_mapping.rb | 23 + lib/orka_api_client/models/remote_image.rb | 41 + lib/orka_api_client/models/remote_iso.rb | 42 + lib/orka_api_client/models/token_info.rb | 31 + lib/orka_api_client/models/user.rb | 122 +++ .../models/vm_configuration.rb | 193 +++++ lib/orka_api_client/models/vm_instance.rb | 468 +++++++++++ lib/orka_api_client/models/vm_resource.rb | 355 +++++++++ lib/orka_api_client/port_mapping.rb | 19 + lib/orka_api_client/version.rb | 12 + orka_api_client.gemspec | 30 + spec/client_spec.rb | 5 + spec/spec_helper.rb | 15 + 41 files changed, 3374 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .parlour create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .yardopts create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.md create mode 100644 Rakefile create mode 100755 bin/console create mode 100755 bin/setup create mode 100644 lib/orka_api_client.rb create mode 100644 lib/orka_api_client/auth_middleware.rb create mode 100644 lib/orka_api_client/client.rb create mode 100644 lib/orka_api_client/connection.rb create mode 100644 lib/orka_api_client/errors.rb create mode 100644 lib/orka_api_client/models/attr_predicate.rb create mode 100644 lib/orka_api_client/models/disk.rb create mode 100644 lib/orka_api_client/models/enumerator.rb create mode 100644 lib/orka_api_client/models/image.rb create mode 100644 lib/orka_api_client/models/iso.rb create mode 100644 lib/orka_api_client/models/kube_account.rb create mode 100644 lib/orka_api_client/models/lazy_model.rb create mode 100644 lib/orka_api_client/models/log_entry.rb create mode 100644 lib/orka_api_client/models/node.rb create mode 100644 lib/orka_api_client/models/password_requirements.rb create mode 100644 lib/orka_api_client/models/protocol_port_mapping.rb create mode 100644 lib/orka_api_client/models/remote_image.rb create mode 100644 lib/orka_api_client/models/remote_iso.rb create mode 100644 lib/orka_api_client/models/token_info.rb create mode 100644 lib/orka_api_client/models/user.rb create mode 100644 lib/orka_api_client/models/vm_configuration.rb create mode 100644 lib/orka_api_client/models/vm_instance.rb create mode 100644 lib/orka_api_client/models/vm_resource.rb create mode 100644 lib/orka_api_client/port_mapping.rb create mode 100644 lib/orka_api_client/version.rb create mode 100644 orka_api_client.gemspec create mode 100644 spec/client_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..86a63dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c85790e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: + - main + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ruby: ['3.1', '3.0', '2.7', '2.6'] + + steps: + - uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Build gem + run: bundle exec rake + + - name: Run RuboCop + run: bundle exec rake rubocop + + - name: Run tests + run: bundle exec rake spec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2056c29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/rbi/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/.parlour b/.parlour new file mode 100644 index 0000000..b374e1d --- /dev/null +++ b/.parlour @@ -0,0 +1,12 @@ +parser: false + +requires: + - sord + +excluded_modules: + - OrkaAPI::AuthMiddleware + +plugins: + Sord::ParlourPlugin: + exclude_untyped: yes + rbi: yes diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..aacc24c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,76 @@ +AllCops: + NewCops: enable + +Layout/CaseIndentation: + EnforcedStyle: end + +Layout/EndAlignment: + EnforcedStyleAlignWith: start_of_line + +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Layout/HashAlignment: + EnforcedHashRocketStyle: table + EnforcedColonStyle: table + +Layout/LineLength: + Max: 118 + +Metrics/AbcSize: + Max: 50 + +Metrics/ClassLength: + Max: 500 + +Metrics/CyclomaticComplexity: + Max: 20 + +Metrics/MethodLength: + Max: 50 + +Metrics/ParameterLists: + CountKeywordArgs: false + +Style/AndOr: + EnforcedStyle: always + +Style/AutoResourceCleanup: + Enabled: true + +Style/CollectionMethods: + Enabled: true + +Style/MutableConstant: + EnforcedStyle: strict + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/StringMethods: + Enabled: true + +Style/SymbolArray: + EnforcedStyle: brackets + +Style/TernaryParentheses: + EnforcedStyle: require_parentheses_when_complex + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +Style/UnlessLogicalOperators: + Enabled: true + EnforcedStyle: forbid_logical_operators diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..ef9dd96 --- /dev/null +++ b/.yardopts @@ -0,0 +1,5 @@ +--no-private +lib/orka_api_client/models/attr_predicate.rb +lib/orka_api_client/models/lazy_model.rb +lib/orka_api_client/client.rb +lib/**/*.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a04709f --- /dev/null +++ b/Gemfile @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in orka_api_client.gemspec +gemspec + +gem "parlour" +gem "rake" +gem "rspec" +gem "rubocop" +gem "rubocop-performance" +gem "rubocop-rake" +gem "rubocop-rspec" +gem "sord", git: "https://github.com/AaronC81/sord", + ref: "25c77951f8d20f73ad336e5d0eb13d0eaad14200" +gem "yard" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..117e4e4 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,101 @@ +GIT + remote: https://github.com/AaronC81/sord + revision: 25c77951f8d20f73ad336e5d0eb13d0eaad14200 + ref: 25c77951f8d20f73ad336e5d0eb13d0eaad14200 + specs: + sord (3.0.1) + commander (~> 4.5) + parlour (~> 5.0) + sorbet-runtime + yard + +PATH + remote: . + specs: + orka_api_client (0.1.0) + faraday (~> 2.0) + faraday-multipart (~> 1.0) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + commander (4.6.0) + highline (~> 2.0.0) + diff-lcs (1.5.0) + faraday (2.1.0) + faraday-net_http (~> 2.0) + ruby2_keywords (>= 0.0.4) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (2.0.1) + highline (2.0.3) + multipart-post (2.1.1) + parallel (1.21.0) + parlour (5.0.0) + commander (~> 4.5) + parser + rainbow (~> 3.0) + sorbet-runtime (>= 0.5) + parser (3.1.0.0) + ast (~> 2.4.1) + rainbow (3.1.1) + rake (13.0.6) + regexp_parser (2.2.0) + rexml (3.2.5) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.3) + rubocop (1.25.0) + parallel (~> 1.10) + parser (>= 3.1.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.15.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.15.1) + parser (>= 3.0.1.1) + rubocop-performance (1.13.2) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (2.8.0) + rubocop (~> 1.19) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sorbet-runtime (0.5.9566) + unicode-display_width (2.1.0) + webrick (1.7.0) + yard (0.9.27) + webrick (~> 1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + orka_api_client! + parlour + rake + rspec + rubocop + rubocop-performance + rubocop-rake + rubocop-rspec + sord! + yard + +BUNDLED WITH + 2.2.32 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b73358 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# orka_api_client + +This is a Ruby library for interacting with MacStadium's [Orka](https://www.macstadium.com/orka) API. + +⚠️⚠️⚠️ **This gem is largely untested beyond basic read-only operations. API stability is not guaranteed at this time.** ⚠️⚠️⚠️ + +## Installation + +**This gem is not yet available on RubyGems.** + +Add this line to your application's Gemfile: + +```ruby +gem 'orka_api_client', git: "https://github.com/Homebrew/orka_api_client" +``` + +And then execute: + + $ bundle install + +## Usage + +TODO: examples + +Documentation is in the `docs` folder after running `bundle exec rake yard`. + +A Sorbet RBI file is available for this gem. + +## Development + +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. + +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). + +RuboCop can be run via `bundle exec rake rubocop`. + +## Tests + +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. + +When they exist, `bundle exec rake spec` can be used to run the tests. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..15388bf --- /dev/null +++ b/Rakefile @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) + +require "rubocop/rake_task" +RuboCop::RakeTask.new + +require "yard" +YARD::Rake::YardocTask.new + +task build: [:date_epoch, :parlour] +task :date_epoch do + ENV["SOURCE_DATE_EPOCH"] = IO.popen(%W[git -C #{__dir__} log -1 --format=%ct], &:read).chomp +end + +task :parlour do + system("bundle exec parlour") || abort +end + +task default: :build diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..705cd6b --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "orka_api_client" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/orka_api_client.rb b/lib/orka_api_client.rb new file mode 100644 index 0000000..257f8e5 --- /dev/null +++ b/lib/orka_api_client.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require_relative "orka_api_client/version" +require_relative "orka_api_client/client" diff --git a/lib/orka_api_client/auth_middleware.rb b/lib/orka_api_client/auth_middleware.rb new file mode 100644 index 0000000..294a849 --- /dev/null +++ b/lib/orka_api_client/auth_middleware.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module OrkaAPI + # @api private + class AuthMiddleware < ::Faraday::Middleware + def initialize(app, token: nil, license_key: nil) + super(app) + + @token = token + @license_key = license_key + end + + def on_request(env) + auth_type = env.request.context&.dig(:orka_auth_type) + + Array(auth_type).each do |type| + case type + when :license + header = "orka-licensekey" + value = @license_key + when :token + header = "Authorization" + value = "Bearer #{@token}" + when nil, :none + next + else + raise AuthConfigurationError, "Invalid Orka auth type." + end + + raise AuthConfigurationError, "Missing #{type} credential." if value.nil? + + next if env.request_headers[header] + + env.request_headers[header] = value + end + end + end +end + +Faraday::Request.register_middleware(orka_auth: OrkaAPI::AuthMiddleware) diff --git a/lib/orka_api_client/client.rb b/lib/orka_api_client/client.rb new file mode 100644 index 0000000..d285ee7 --- /dev/null +++ b/lib/orka_api_client/client.rb @@ -0,0 +1,739 @@ +# frozen_string_literal: true + +require_relative "errors" +require_relative "connection" +require_relative "models/enumerator" +require_relative "models/user" +require_relative "models/vm_configuration" +require_relative "models/vm_resource" +require_relative "models/node" +require_relative "models/image" +require_relative "models/remote_image" +require_relative "models/iso" +require_relative "models/remote_iso" +require_relative "models/kube_account" +require_relative "models/log_entry" +require_relative "models/token_info" +require_relative "models/password_requirements" + +module OrkaAPI + # This is the entrypoint class for all interactions with the Orka API. + class Client + # Creates an instance of the client for a given Orka service endpoint and associated credentials. + # + # @param [String] base_url The API URL for the Orka service endpoint. + # @param [String] token The token used for authentication. This can be generated with {#create_token} from an + # credentialless client. + # @param [String] license_key The Orka license key used for authentication in administrative operations. + def initialize(base_url, token: nil, license_key: nil) + @conn = Connection.new(base_url, token: token, license_key: license_key) + @license_key = license_key + end + + # @!macro [new] lazy_enumerator + # The network operation is not performed immediately upon return of this method. The request is performed when + # any action is performed on the enumerator, or otherwise forced via {Models::Enumerator#eager}. + + # @!macro [new] lazy_object + # The network operation is not performed immediately upon return of this method. The request is performed when + # any attribute is accessed or any method is called on the returned object, or otherwise forced via + # {Models::LazyModel#eager}. Successful return from this method does not guarantee the requested resource + # exists. + + # @!macro [new] auth_none + # This method does not require the client to be configured with any credentials. + + # @!macro [new] auth_token + # This method requires the client to be configured with a token. + + # @!macro [new] auth_license + # This method requires the client to be configured with a license key. + + # @!macro [new] auth_token_and_license + # This method requires the client to be configured with both a token and a license key. + + # @!group Users + + # Retrieve a list of the users in the Orka environment. + # + # @macro auth_license + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the user list. + def users + Models::Enumerator.new do + users = [] + groups = @conn.get("users") do |r| + r.options.context = { + orka_auth_type: :license, + } + end.body["user_groups"] + groups.each do |group, group_users| + group_users.each do |group_user| + users << Models::User.new( + conn: @conn, + email: group_user, + group: group, + ) + end + end + users + end + end + + # Fetches information on a particular user in the Orka environment. + # + # @macro auth_license + # + # @macro lazy_object + # + # @param [String] email The email of the user to fetch. + # @return [Models::User] The lazily-loaded user object. + def user(email) + Models::User.lazy_prepare(email: email, conn: @conn) + end + + # Create a new user in the Orka environment. You need to specify email address and password. You cannot pass an + # email address that's already in use. + # + # @macro auth_license + # + # @param [String] email An email address for the user. This also serves as the username. + # @param [String] password A password for the user. Must be at least 6 characters long. + # @param [String] group A user group for the user. Once set, you can no longer change the user group. + # @return [Models::User] The user object. + def create_user(email:, password:, group: nil) + body = { + email: email, + password: password, + group: group, + }.compact + @conn.post("users", body) do |r| + r.options.context = { + orka_auth_type: :license, + } + end + + group = "$ungrouped" if group.nil? + Models::User.new(conn: @conn, email: group_user, group: group) + end + + # Modify the email address or password of the current user. This operation is intended for regular Orka users. + # + # @macro auth_token + # + # @param [String] email The new email address for the user. + # @param [String] password The new password for the user. + # @return [void] + def update_user_credentials(email: nil, password: nil) + raise ArgumentError, "Must update either the username or password, or both." if email.nil? && password.nil? + + body = { + email: email, + password: password, + }.compact + @conn.put("users", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # @!endgroup + # @!group Tokens + + # Create an authentication token using an existing user's email and password. + # + # @macro auth_none + # + # @param [Models::User, String] user The user or their associated email address. + # @param [String] password The user's password. + # @return [String] The authentication token. + def create_token(user:, password:) + body = { + email: user_email(user), + password: password, + }.compact + @conn.post("token", body).body["token"] + end + + # Revoke the token associated with this client instance. + # + # @macro auth_token + # + # @return [void] + def revoke_token + @conn.delete("token") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # @!endgroup + # @!group VMs + + # Retrieve a list of the VMs and VM configurations. By default this fetches resources associated with the + # client's token, but you can optionally request a list of resources for another user (or all users). + # + # If you filter by a user, or request all users, this method requires the client to be configured with both a + # token and a license key. Otherwise, it only requires a token. + # + # @macro lazy_enumerator + # + # @param [Models::User, String] user The user, or their associated email address, to use instead of the one + # associated with the client's token. Pass "all" if you wish to fetch for all users. + # @return [Models::Enumerator] The enumerator of the VM resource list. + def vm_resources(user: nil) + Models::Enumerator.new do + url = "resources/vm/list" + url += "/#{user}" unless user.nil? + resources = @conn.get(url) do |r| + auth_type = [:token] + auth_type << :license unless user.nil? + r.options.context = { + orka_auth_type: auth_type, + } + end.body["virtual_machine_resources"] + resources.map { |hash| Models::VMResource.from_hash(hash, conn: @conn, admin: !user.nil?) } + end + end + + # Fetches information on a particular VM or VM configuration. + # + # If you set the admin parameter to true, this method requires the client to be configured with both a + # token and a license key. Otherwise, it only requires a token. + # + # @macro lazy_object + # + # @param [String] name The name of the VM resource to fetch. + # @param [Boolean] admin Set to true to allow VM resources associated with other users to be queried. + # @return [Models::VMResource] The lazily-loaded VM resource object. + def vm_resource(name, admin: false) + Models::VMResource.lazy_prepare(name: name, conn: @conn, admin: admin) + end + + # Retrieve a list of the VM configurations associated with the client's token. Orka returns information about the + # base image, CPU cores, owner and name of the VM configurations. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the VM configuration list. + def vm_configurations + Models::Enumerator.new do + configs = @conn.get("resources/vm/configs") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["configs"] + configs.map { |hash| Models::VMConfiguration.from_hash(hash, conn: @conn) } + end + end + + # Fetches information on a particular VM configuration. + # + # @macro auth_token + # + # @macro lazy_object + # + # @param [String] name The name of the VM configuration to fetch. + # @return [Models::VMConfiguration] The lazily-loaded VM configuration. + def vm_configuration(name) + Models::VMConfiguration.lazy_prepare(name: name, conn: @conn) + end + + # Create a VM configuration that is ready for deployment. In Orka, VM configurations are container templates. + # You can deploy multiple VMs from a single VM configuration. You cannot modify VM configurations. + # + # @macro auth_token + # + # @param [String] name The name of the VM configuration. This string must consist of lowercase Latin alphanumeric + # characters or the dash (+-+). This string must begin and end with an alphanumeric character. This string must + # not exceed 38 characters. + # @param [Models::Image, String] base_image The name of the base image that you want to use with the + # configuration. If you want to attach an ISO to the VM configuration from which to install macOS, make sure + # that the base image is an empty disk of a sufficient size. + # @param [Models::Image, String] snapshot_image A name for the + # {https://orkadocs.macstadium.com/docs/orka-glossary#section-snapshot-image snapshot image} of the VM. + # Typically, the same value as +name+. + # @param [Integer] cpu_cores The number of CPU cores to dedicate for the VM. Must be 3, 4, 6, 8, 12, or 24. + # @param [Integer] vcpu_count The number of vCPUs for the VM. Must equal the number of CPUs, when CPU is less + # than or equal to 3. Otherwise, must equal half of or exactly the number of CPUs specified. + # @param [Models::ISO, String] iso_image An ISO to attach to the VM on deployment. + # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM on deployment. + # @param [Boolean] vnc_console By default, +true+. Enables or disables VNC for the VM configuration. You can + # override on deployment of specific VMs. + # @param [String] system_serial Assign an owned macOS system serial number to the VM configuration. + # @param [Boolean] io_boost By default, +false+ for VM configurations created before Orka 1.5. Default value for + # VM configurations created with Orka 1.5 or later depends on the cluster default. Enables or disables IO + # performance improvements for the VM configuration. + # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. When enabled, +vnc_console+ is + # automatically disabled. GPU passthrough is an experimental feature. GPU passthrough must first be enabled in + # your cluster. + # @return [Models::VMConfiguration] The lazily-loaded VM configuration. + def create_vm_configuraton(name, + base_image:, snapshot_image:, cpu_cores:, vcpu_count:, + iso_image: nil, attached_disk: nil, vnc_console: nil, + system_serial: nil, io_boost: nil, gpu_passthrough: nil) + body = { + orka_vm_name: name, + orka_base_image: base_image.is_a?(Models::Image) ? base_image.name : base_image, + orka_image: snapshot_image.is_a?(Models::Image) ? snapshot_image.name : snapshot_image, + orka_cpu_core: cpu_cores, + vcpu_count: vcpu_count, + iso_image: iso_image.is_a?(Models::ISO) ? iso_image.name : iso_image, + attached_disk: attached_disk.is_a?(Models::Image) ? attached_disk.name : attached_disk, + vnc_console: vnc_console, + system_serial: system_serial, + io_boost: io_boost, + gpu_passthrough: gpu_passthrough, + }.compact + @conn.post("resources/vm/create", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + vm_configuration(name) + end + + # @!endgroup + # @!group Nodes + + # Retrieve a list of the nodes in your Orka environment. Orka returns a list of nodes with IP and resource + # information. + # + # If you set the admin parameter to true, this method requires the client to be configured with both a + # token and a license key. Otherwise, it only requires a token. + # + # @macro lazy_enumerator + # + # @param [Boolean] admin Set to true to allow nodes dedicated to other users to be queried. + # @return [Models::Enumerator] The enumerator of the node list. + def nodes(admin: false) + Models::Enumerator.new do + url = "resources/node/list" + url += "/all" if admin + nodes = @conn.get(url) do |r| + auth_type = [:token] + auth_type << :license if admin + r.options.context = { + orka_auth_type: auth_type, + } + end.body["nodes"] + nodes.map { |hash| Models::Node.from_hash(hash, conn: @conn, admin: admin) } + end + end + + # Fetches information on a particular node. + # + # If you set the admin parameter to true, this method requires the client to be configured with both a + # token and a license key. Otherwise, it only requires a token. + # + # @macro lazy_object + # + # @param [String] name The name of the node to fetch. + # @param [Boolean] admin Set to true to allow nodes dedicated with other users to be queried. + # @return [Models::VMResource] The lazily-loaded node object. + def node(name, admin: false) + Models::Node.lazy_prepare(name: name, conn: @conn, admin: admin) + end + + # @!endgroup + # @!group Images + + # Retrieve a list of the base images and empty disks in your Orka environment. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the image list. + def images + Models::Enumerator.new do + images = @conn.get("resources/image/list") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["image_attributes"] + images.map { |hash| Models::Image.from_hash(hash, conn: @conn) } + end + end + + # Fetches information on a particular image. + # + # @macro auth_token + # + # @macro lazy_object + # + # @param [String] name The name of the image to fetch. + # @return [Models::Image] The lazily-loaded image. + def image(name) + Models::Image.lazy_prepare(name: name, conn: @conn) + end + + # List the base images available in the Orka remote repo. + # + # To use one of the images from the remote repo, you can {Models::RemoteImage#pull pull} it into the local Orka + # storage. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the remote image list. + def remote_images + Models::Enumerator.new do + images = @conn.get("resources/image/list-remote") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["images"] + images.map { |name| Models::RemoteImage.new(name, conn: @conn) } + end + end + + # Returns an object representing a remote image of a specified name. + # + # Note that this method does not perform any network requests and does not verify if the name supplied actually + # exists in the Orka remote repo. + # + # @param [String] name The name of the remote image. + # @return [Models::RemoteImage] The remote image object. + def remote_image(name) + Models::RemoteImage.new(name, conn: @conn) + end + + # Generate an empty base image. You can use it to create VM configurations that will use an ISO or you can attach + # it to a deployed VM to extend its storage. + # + # @macro auth_token + # + # @param [String] name The name of this new image. + # @param [String] size The size of this new image (in K, M, G, or T), for example +"10G"+. + # @return [Models::Image] The new lazily-loaded image. + def generate_empty_image(name, size:) + body = { + file_name: name, + file_size: size, + }.compact + @conn.post("resources/image/generate", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + image(name) + end + + # Upload an image to the Orka environment. + # + # @macro auth_token + # + # @param [String, IO] file The string file path or an open IO object to the image to upload. + # @param [String] name The name to give to this image. Defaults to the local filename. + # @return [Models::Image] The new lazily-loaded image. + def upload_image(file, name: nil) + file_part = Faraday::Multipart::FilePart.new( + file, + "application/x-iso9660-image", + name, + ) + body = { image: file_part } + @conn.post("resources/image/upload", body, "Content-Type" => "multipart/form-data") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + image(file_part.original_filename) + end + + # @!endgroup + # @!group Images + + # Retrieve a list of the ISOs available in the local Orka storage. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the ISO list. + def isos + Models::Enumerator.new do + isos = @conn.get("resources/iso/list") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["iso_attributes"] + isos.map { |hash| Models::ISO.from_hash(hash, conn: @conn) } + end + end + + # Fetches information on a particular ISO in local Orka storage. + # + # @macro auth_token + # + # @macro lazy_object + # + # @param [String] name The name of the ISO to fetch. + # @return [Models::ISO] The lazily-loaded ISO. + def iso(name) + Models::ISO.lazy_prepare(name: name, conn: @conn) + end + + # List the ISOs available in the Orka remote repo. + # + # To use one of the ISOs from the remote repo, you can {Models::RemoteISO#pull pull} it into the local Orka + # storage. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Models::Enumerator] The enumerator of the remote ISO list. + def remote_isos + Models::Enumerator.new do + isos = @conn.get("resources/iso/list-remote") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["isos"] + isos.map { |name| Models::RemoteISO.new(name, conn: @conn) } + end + end + + # Returns an object representing a remote ISO of a specified name. + # + # Note that this method does not perform any network requests and does not verify if the name supplied actually + # exists in the Orka remote repo. + # + # @param [String] name The name of the remote ISO. + # @return [Models::RemoteISO] The remote ISO object. + def remote_iso(name) + Models::RemoteISO.new(name, conn: @conn) + end + + # Upload an ISO to the Orka environment. + # + # @macro auth_token + # + # @param [String, IO] file The string file path or an open IO object to the ISO to upload. + # @param [String] name The name to give to this ISO. Defaults to the local filename. + # @return [Models::ISO] The new lazily-loaded ISO. + def upload_iso(file, name: nil) + file_part = Faraday::Multipart::FilePart.new( + file, + "application/x-iso9660-image", + name, + ) + body = { iso: file_part } + @conn.post("resources/iso/upload", body, "Content-Type" => "multipart/form-data") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + iso(file_part.original_filename) + end + + # @!endgroup + # @!group Kube-Accounts + + # Retrieve a list of kube-accounts associated with an Orka user. + # + # @macro auth_token_and_license + # + # @macro lazy_enumerator + # + # @param [Models::User, String] user The user, which can be specified by the user object or their email address, + # for which we are returning the associated kube-accounts of. Defaults to the user associated with the client's + # token. + # @return [Models::Enumerator] The enumerator of the kube-account list. + def kube_accounts(user: nil) + Models::Enumerator.new do + accounts = @conn.get("resources/kube-account") do |r| + email = user_email(user) + r.body = { + email: email, + }.compact + + r.options.context = { + orka_auth_type: [:token, :license], + } + end.body["serviceAccounts"] + accounts.map { |name| Models::KubeAccount.new(name, email: email, conn: @conn) } + end + end + + # Returns an object representing a kube-account of a particular user. + # + # Note that this method does not perform any network requests and does not verify if the name supplied actually + # exists in the Orka environment. + # + # @param [String] name The name of the kube-account. + # @param [Models::User, String] user The user, which can be specified by the user object or their email address, + # of which the kube-account is associated with. Defaults to the user associated with the client's token. + # @return [Models::KubeAccount] The kube-account object. + def kube_account(name, user: nil) + Models::KubeAccount.new(name, email: user_email(user), conn: @conn) + end + + # Create a kube-account. + # + # @macro auth_token_and_license + # + # @param [String] name The name of the kube-account. + # @param [Models::User, String] user The user, which can be specified by the user object or their email address, + # of which the kube-account will be associated with. Defaults to the user associated with the client's token. + # @return [Models::KubeAccount] The created kube-account. + def create_kube_account(name, user: nil) + email = user_email(user) + body = { + name: name, + email: email, + }.compact + kubeconfig = @conn.post("resources/kube-account", body) do |r| + r.options.context = { + orka_auth_type: [:token, :license], + } + end.body["kubeconfig"] + Models::KubeAccount.new(name, email: email, kubeconfig: kubeconfig, conn: @conn) + end + + # Delete all kube-accounts associated with a user. + # + # @macro auth_token_and_license + # + # @param [Models::User, String] user The user, which can be specified by the user object or their email address, + # which will have their associated kube-account deleted. Defaults to the user associated with the client's + # token. + # @return [void] + def delete_all_kube_accounts(user: nil) + @conn.delete("resources/kube-account") do |r| + email = user_email(user) + r.body = { + email: email, + }.compact + + r.options.context = { + orka_auth_type: [:token, :license], + } + end + end + + # @!endgroup + # @!group Logs + + # Retrieve a log of all CLI commands and API requests executed against your Orka environment. + # + # @macro auth_license + # + # @macro lazy_enumerator + # + # @param [Integer] limit Limit the amount of results returned to this amount. + # @return [Models::Enumerator] The enumerator of the log entries list. + def logs(limit: nil) + Models::Enumerator.new do + logs = @conn.post("logs/query") do |r| + r.params[:limit] = limit unless limit.nil? + + r.options.context = { + orka_auth_type: :license, + } + end.body["logs"] + logs.map { |hash| Models::LogEntry.new(hash) } + end + end + + # Delete all logs for your Orka environment. + # + # @macro auth_token_and_license + # + # @return [void] + def delete_logs + @conn.delete("logs") do |r| + r.options.context = { + orka_auth_type: [:license, :token], + } + end + end + + # @!endgroup + # @!group Environment Checks + + # Retrieve information about the token associated with the client. The request returns information about the + # associated email address, the authentication status of the token, and if the token is revoked. + # + # @macro auth_token + # + # @return [Models::TokenInfo] Information about the token. + def token_info + body = @conn.get("token") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body + Models::TokenInfo.new(body, conn: @conn) + end + + # Retrieve the current API version of your Orka environment. + # + # @macro auth_none + # + # @return [String] The remote API version. + def remote_api_version + @conn.get("health-check").body["api_version"] + end + + # Retrieve the current password requirements for creating an Orka user. + # + # @macro auth_none + # + # @return [Models::PasswordRequirements] The password requirements. + def password_requirements + Models::PasswordRequirements.new(@conn.get("validation-requirements").body) + end + + # Check if a license key is authorized or not. + # + # @macro auth_none + # + # @param [String] license_key The license key to check. Defaults to the one associated with the client. + # @return [Boolean] True if the license key is valid. + def license_key_valid?(license_key = @license_key) + raise ArgumentError, "License key is required." if license_key.nil? + + @conn.get("validate-license-key") do |r| + r.body = { + licenseKey: license_key, + } + end + true + rescue Faraday::UnauthorizedError + false + end + + # Retrieve the default base image for the Orka environment. + # + # @macro auth_none + # + # @return [Models::Image] The lazily-loaded default base image object. + def default_base_image + Image.lazy_prepare(name: @conn.get("default-base-image").body["default_base_image"], conn: @conn) + end + + # @!endgroup + + alias inspect to_s + + private + + def user_email(user) + if user.is_a?(Models::User) + user.email + else + user + end + end + end +end diff --git a/lib/orka_api_client/connection.rb b/lib/orka_api_client/connection.rb new file mode 100644 index 0000000..1686c99 --- /dev/null +++ b/lib/orka_api_client/connection.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "faraday" +require "faraday/multipart" +require_relative "auth_middleware" + +module OrkaAPI + # @api private + class Connection < ::Faraday::Connection + # @param [String] base_url + # @param [String] token + # @param [String] license_key + def initialize(base_url, token: nil, license_key: nil) + super( + url: base_url, + headers: { + "User-Agent" => "HomebrewOrkaClient/#{Client::VERSION}", + } + ) do |f| + f.request :orka_auth, token: token, license_key: license_key + f.request :json + f.request :multipart + f.response :json + f.response :raise_error # TODO: wrap this ourselves + end + end + + alias inspect to_s + end +end diff --git a/lib/orka_api_client/errors.rb b/lib/orka_api_client/errors.rb new file mode 100644 index 0000000..f5b8aa7 --- /dev/null +++ b/lib/orka_api_client/errors.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module OrkaAPI + # Base error class. + class Error < ::StandardError; end + + # This error is thrown if an endpoint requests an auth mechanism which we do not have credentials for. + class AuthConfigurationError < Error; end + + # This error is thrown if a specific resource is requested but it was not found in the Orka backend. + class ResourceNotFoundError < Error; end + + # This error is thrown if the client receives data from the server it does not recognise. This is typically + # indicative of a bug or a feature not yet implemented. + class UnrecognisedStateError < Error; end +end diff --git a/lib/orka_api_client/models/attr_predicate.rb b/lib/orka_api_client/models/attr_predicate.rb new file mode 100644 index 0000000..3462b69 --- /dev/null +++ b/lib/orka_api_client/models/attr_predicate.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # @private + module AttrPredicate + private + + # @!parse + # # @!macro [attach] attr_predicate + # # @!attribute [r] $1? + # def self.attr_predicate(*); end + def attr_predicate(*attrs) + attrs.each do |attr| + define_method "#{attr}?" do + instance_variable_get("@#{attr}") + end + end + end + end + end +end diff --git a/lib/orka_api_client/models/disk.rb b/lib/orka_api_client/models/disk.rb new file mode 100644 index 0000000..612fb7d --- /dev/null +++ b/lib/orka_api_client/models/disk.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # Information on a disk attached to a VM. + class Disk + # @return [String] + attr_reader :type + + # @return [String] + attr_reader :device + + # @return [String] + attr_reader :target + + # @return [String] + attr_reader :source + + # @api private + # @param [String] type + # @param [String] device + # @param [String] target + # @param [String] source + def initialize(type:, device:, target:, source:) + @type = type + @device = device + @target = target + @source = source + end + end + end +end diff --git a/lib/orka_api_client/models/enumerator.rb b/lib/orka_api_client/models/enumerator.rb new file mode 100644 index 0000000..40c67cb --- /dev/null +++ b/lib/orka_api_client/models/enumerator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # Enumerator subclass for networked operations. + class Enumerator < ::Enumerator + # @api private + def initialize + super do |yielder| + yield.each do |item| + yielder << item + end + end + end + + # Forces this lazily-loaded enumerator to be fully loaded, peforming any necessary network operations. + # + # @return [self] + def eager + begin + peek + rescue StopIteration + # We're fine if the enuemrator is empty. + end + self + end + end + end +end diff --git a/lib/orka_api_client/models/image.rb b/lib/orka_api_client/models/image.rb new file mode 100644 index 0000000..b7eb39f --- /dev/null +++ b/lib/orka_api_client/models/image.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require_relative "lazy_model" + +module OrkaAPI + module Models + # A disk image that represents a VM's storage and its contents, including the OS and any installed software. + class Image < LazyModel + # @return [String] The name of this image. + attr_reader :name + + # @return [String] The size of this image. Orka lists generated empty storage disks with a fixed size of ~192k. + # When attached to a VM and formatted, the disk will appear with its correct size in the OS. + lazy_attr :size + + # @return [DateTime] The time this image was last modified. + lazy_attr :modification_time + + # @return [DateTime] The time this image was first created. + lazy_attr :creation_time + + # @return [String] + lazy_attr :owner + + # @api private + # @param [String] name + # @param [Connection] conn + # @return [Image] + def self.lazy_prepare(name:, conn:) + new(conn: conn, name: name) + end + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @return [Image] + def self.from_hash(hash, conn:) + new(conn: conn, hash: hash) + end + + # @private + # @param [Connection] conn + # @param [String] name + # @param [Hash] hash + def initialize(conn:, name: nil, hash: nil) + super(!hash.nil?) + @conn = conn + @name = name + deserialize(hash) if hash + end + + # Rename this image. + # + # @macro auth_token + # + # @note After you rename a base image, you can no longer deploy any VM configurations that are based on the + # image of the old name. + # + # @param [String] new_name The new name for this image. + # @return [void] + def rename(new_name) + body = { + image: @name, + new_name: new_name, + }.compact + @conn.post("resources/image/rename", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + @name = new_name + end + + # Copy this image to a new one. + # + # @macro auth_token + # + # @param [String] new_name The name for the copy of this image. + # @return [Image] The lazily-loaded image copy. + def copy(new_name) + body = { + image: @name, + new_name: new_name, + }.compact + @conn.post("resources/image/copy", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + Image.lazy_prepare(new_name, conn: @conn) + end + + # Delete this image from the local Orka storage. + # + # @macro auth_token + # + # @note Make sure that the image is not in use. + # + # @return [void] + def delete + body = { + image: @name, + }.compact + @conn.post("resources/image/delete", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Download this image from Orka cluster storage to your local filesystem. + # + # @macro auth_token + # + # @param [String, Pathname, IO] to An open IO, or a String/Pathname file path to the file or directory where + # you want the image to be written. + # @return [void] + def download(to:) + io_input = to.is_a?(::IO) + file = if io_input + to + else + to = File.join(to, @name) if File.directory?(to) + File.open(to, "wb:ASCII-8BIT") + end + @conn.get("resources/image/download/#{@name}") do |r| + r.options.context = { + orka_auth_type: :token, + } + + r.options.on_data = proc do |chunk, _| + file.write(chunk) + end + end + ensure + file.close unless io_input + end + + # Request the MD5 file checksum for this image in Orka cluster storage. The checksum can be used to verify file + # integrity for a downloaded or uploaded image. + # + # @macro auth_token + # + # @return [String, nil] The MD5 checksum of the image, or nil if the calculation is in progress and has not + # completed. + def checksum + @conn.get("resources/image/checksum/#{@name}") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body&.dig("checksum") + end + + private + + def lazy_initialize + response = @conn.get("resources/image/list") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + image = response.body["image_attributes"].find { |hash| hash["image"] == @name } + + raise ResourceNotFoundError, "No image found matching \"#{@name}\"." if image.nil? + + deserialize(image) + super + end + + def deserialize(hash) + @name = hash["image"] + @size = hash["image_size"] + @modification_time = DateTime.iso8601(hash["modified"]) + @creation_time = DateTime.iso8601(hash["date_added"]) + @owner = hash["owner"] + end + end + end +end diff --git a/lib/orka_api_client/models/iso.rb b/lib/orka_api_client/models/iso.rb new file mode 100644 index 0000000..f76c523 --- /dev/null +++ b/lib/orka_api_client/models/iso.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require_relative "lazy_model" + +module OrkaAPI + module Models + # An +.iso+ disk image used exclusively for the installation of macOS on a virtual machine. You must attach the + # ISO to the VM during deployment. After the installation is complete and the VM has booted successfully, you + # need to restart the VM to detach the ISO. + class ISO < LazyModel + # @return [String] The name of this ISO. + attr_reader :name + + # @return [String] The size of this ISO. + lazy_attr :size + + # @return [DateTime] The time this image was last modified. + lazy_attr :modification_time + + # @api private + # @param [String] name + # @param [Connection] conn + # @return [ISO] + def self.lazy_prepare(name:, conn:) + new(conn: conn, name: name) + end + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @return [ISO] + def self.from_hash(hash, conn:) + new(conn: conn, hash: hash) + end + + # @private + # @param [Connection] conn + # @param [String] name + # @param [Hash] hash + def initialize(conn:, name: nil, hash: nil) + super(!hash.nil?) + @conn = conn + @name = name + deserialize(hash) if hash + end + + # Rename this ISO. + # + # @macro auth_token + # + # @note Make sure that the ISO is not in use. Any VMs that have the ISO of the old name attached will no longer + # be able to boot from it. + # + # @param [String] new_name The new name for this ISO. + # @return [void] + def rename(new_name) + body = { + iso: @name, + new_name: new_name, + }.compact + @conn.post("resources/iso/rename", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + @name = new_name + end + + # Copy this ISO to a new one. + # + # @macro auth_token + # + # @param [String] new_name The name for the copy of this ISO. + # @return [Image] The lazily-loaded ISO copy. + def copy(new_name) + body = { + iso: @name, + new_name: new_name, + }.compact + @conn.post("resources/iso/copy", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + ISO.lazy_prepare(new_name, conn: @conn) + end + + # Delete this ISO from the local Orka storage. + # + # @macro auth_token + # + # @note Make sure that the ISO is not in use. Any VMs that have the ISO attached will no longer be able to boot + # from it. + # + # @return [void] + def delete + body = { + iso: @name, + }.compact + @conn.post("resources/iso/delete", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + private + + def lazy_initialize + response = @conn.get("resources/iso/list") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + iso = response.body["iso_attributes"].find { |hash| hash["iso"] == @name } + + raise ResourceNotFoundError, "No ISO found matching \"#{@name}\"." if iso.nil? + + deserialize(iso) + super + end + + def deserialize(hash) + @name = hash["iso"] + @size = hash["iso_size"] + @modification_time = DateTime.iso8601(hash["modified"]) + end + end + end +end diff --git a/lib/orka_api_client/models/kube_account.rb b/lib/orka_api_client/models/kube_account.rb new file mode 100644 index 0000000..514afe6 --- /dev/null +++ b/lib/orka_api_client/models/kube_account.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # An account used for Kubernetes operations. + class KubeAccount + # @return [String] The name of this kube-account. + attr_reader :name + + # @api private + # @param [String] name + # @param [String] email + # @param [String] kubeconfig + # @param [Connection] conn + def initialize(name, conn:, email: nil, kubeconfig: nil) + @name = name + @email = email + @kubeconfig = kubeconfig + @conn = conn + end + + # Regenerate this kube-account. + # + # @macro auth_token_and_license + # + # @return [void] + def regenerate + body = { + name: name, + email: email, + }.compact + @kubeconfig = @conn.post("resources/kube-account/regenerate", body) do |r| + r.options.context = { + orka_auth_type: [:token, :license], + } + end.body["kubeconfig"] + end + + # Retrieve the +kubeconfig+ for this kube-account. + # + # This method is cached. Subsequent calls to this method will not invoke additional network requests. The + # methods {#regenerate} and {Client#create_kube_account} also fill this cache. + # + # @macro auth_token_and_license + # + # @return [void] + def kubeconfig + return @kubeconfig unless @kubeconfig.nil? + + @kubeconfig = @conn.get("resources/kube-account/download") do |r| + r.body = { + name: @name, + email: @email, + }.compact + + r.options.context = { + orka_auth_type: [:token, :license], + } + end.body["@kubeconfig"] + end + end + end +end diff --git a/lib/orka_api_client/models/lazy_model.rb b/lib/orka_api_client/models/lazy_model.rb new file mode 100644 index 0000000..66bb77f --- /dev/null +++ b/lib/orka_api_client/models/lazy_model.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # The base class for lazily-loaded objects. + class LazyModel + # @!macro [attach] lazy_attr + # @!attribute [r] + def self.lazy_attr(*attrs) + attrs.each do |attr| + define_method attr do + ivar = "@#{attr.to_s.delete_suffix("?")}" + existing = instance_variable_get(ivar) + return existing unless existing.nil? + + eager + instance_variable_get(ivar) + end + end + end + private_class_method :lazy_attr + + # @private + # @param [Boolean] lazy_initialized + def initialize(lazy_initialized) + @lazy_initialized = lazy_initialized + end + private_class_method :new + + # Forces this lazily-loaded object to be fully loaded, peforming any necessary network operations. + # + # @return [self] + def eager + lazy_initialize unless @lazy_initialized + self + end + + # Re-fetches this object's data from the Orka API. This will raise an error if the object no longer exists. + # + # @return [void] + def refresh + lazy_initialize + end + + private + + def lazy_initialize + @lazy_initialized = true + end + end + end +end diff --git a/lib/orka_api_client/models/log_entry.rb b/lib/orka_api_client/models/log_entry.rb new file mode 100644 index 0000000..7a4ba78 --- /dev/null +++ b/lib/orka_api_client/models/log_entry.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # A particular log event. + class LogEntry + # Information on the request element of a log event. + class Request + # @return [Hash] The body of the request. + attr_reader :body + + # @return [Hash{String => String}] The headers of the request. + attr_reader :headers + + # @return [String] The HTTP method used for the request. + attr_reader :method + + # @return [String] The request URL. + attr_reader :url + + # @api private + # @param [Hash] hash + def initialize(hash) + @body = hash["body"] + @headers = hash["headers"] + @method = hash["method"] + @url = hash["url"] + end + end + + # Information on the response element of a log event. + class Response + # @return [Hash] The body of the response. + attr_reader :body + + # @return [Hash{String => String}] The headers of the response. + attr_reader :headers + + # @return [Integer] The resultant HTTP status code of the response. + attr_reader :status_code + + # @api private + # @param [Hash] hash + def initialize(hash) + @body = hash["body"] + @headers = hash["headers"] + @status_code = hash["statusCode"] + end + end + + # Information on the user responsible for the log event. + class User + # @return [String] The user's email. + attr_reader :email + + # @return [String] The user's ID. + attr_reader :id + + # @api private + # @param [Hash] hash + def initialize(hash) + @email = hash["email"] + @id = hash["id"] + end + end + + # @return [DateTime] The time the log entry was created. + attr_reader :creation_time + + # @return [String] The ID of the log entry. + attr_reader :id + + # @return [Request] The HTTP request made. + attr_reader :request + + # @return [Response] The HTTP response returned. + attr_reader :response + + # @return [LogEntry::User, nil] The user which performed the action, if authenticated. + attr_reader :user + + # @api private + # @param [Hash] hash + def initialize(hash) + raise UnrecognisedStateError, "Unknown log version." if hash["logVersion"] != "1.0" + + @creation_time = DateTime.iso8601(hash["createdAt"]) + @id = hash["id"] + @request = Request.new(hash["request"]) + @response = Response.new(hash["response"]) + @user = if hash["user"].empty? + nil + else + User.new(hash["user"]) + end + end + end + end +end diff --git a/lib/orka_api_client/models/node.rb b/lib/orka_api_client/models/node.rb new file mode 100644 index 0000000..b218818 --- /dev/null +++ b/lib/orka_api_client/models/node.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +require_relative "lazy_model" +require_relative "protocol_port_mapping" + +module OrkaAPI + module Models + # A physical or logical host that provides computational resources for your VMs. Usually, an Orka node is a + # genuine Apple physical host with a host OS on top. You have no direct access (via VNC, SSH, or Screen Sharing) + # to your nodes. + class Node < LazyModel + # @return [String] The name of this node. + attr_reader :name + + # @return [String] The host name of this node. + lazy_attr :host_name + + # @return [String] The IP address of this node. + lazy_attr :address + + # @return [String] The host IP address of this node. + lazy_attr :host_ip + + # @return [Integer] The number of free CPU cores on this node. + lazy_attr :available_cpu + + # @return [Integer] The total number of CPU cores on this node that are allocatable to VMs. + lazy_attr :allocatable_cpu + + # @return [String] The amount of free RAM on this node. + lazy_attr :available_memory + + # @return [Integer] The total number of CPU cores on this node. + lazy_attr :total_cpu + + # @return [String] The total amount of RAM on this node. + lazy_attr :total_memory + + # @return [String] The state of this node. + lazy_attr :state + + # @return [String, nil] The user group this node is dedicated to, if any. + lazy_attr :orka_group + + # @api private + # @param [String] name + # @param [Connection] conn + # @param [Boolean] admin + # @return [Node] + def self.lazy_prepare(name:, conn:, admin: false) + new(conn: conn, name: name, admin: admin) + end + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @param [Boolean] admin + # @return [Node] + def self.from_hash(hash, conn:, admin: false) + new(conn: conn, hash: hash, admin: admin) + end + + # @private + # @param [Connection] conn + # @param [String] name + # @param [Hash] hash + # @param [Boolean] admin + def initialize(conn:, name: nil, hash: nil, admin: false) + super(!hash.nil?) + @conn = conn + @name = name + @admin = admin + deserialize(hash) if hash + end + + # Get a detailed list of all reserved ports on this node. Orka lists them as port mappings between + # {ProtocolPortMapping#host_port host_port} and {ProtocolPortMapping#guest_port guest_port}. + # {ProtocolPortMapping#host_port host_port} indicates a port on the node, {ProtocolPortMapping#guest_port + # guest_port} indicates a port on a VM on this node. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Enumerator] The enumerator of the reserved ports list. + def reserved_ports + Enumerator.new do + all_ports = @conn.get("resources/ports") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["reserved_ports"] + + node_ports = all_ports.select do |hash| + hash["orka_node_name"] == @name + end + + node_ports.map do |mapping| + ProtocolPortMapping.new( + host_port: mapping["host_port"], + guest_port: mapping["guest_port"], + protocol: mapping["protocol"], + ) + end + end + end + + # Tag this node as sandbox. This limits deployment management from the Orka CLI. You can perform only + # Kubernetes deployment management with +kubectl+, {https://helm.sh/docs/helm/#helm Helm}, and Tiller. + # + # @macro auth_token_and_license + # + # @return [void] + def enable_sandbox + body = { + orka_node_name: @name, + }.compact + @conn.post("resources/node/sandbox", body) do |r| + r.options.context = { + orka_auth_type: [:license, :token], + } + end + end + + # Remove the sandbox tag from this node. This re-enables deployment management with the Orka CLI. + # + # @macro auth_token_and_license + # + # @return [void] + def disable_sandbox + @conn.delete("resources/node/sandbox") do |r| + r.body = { + orka_node_name: @name, + }.compact + + r.options.context = { + orka_auth_type: [:license, :token], + } + end + end + + # Dedicate this node to a specified user group. Only users from this user group will be able to deploy to the + # node. + # + # @macro auth_token_and_license + # + # @note This is a BETA feature. + # + # @param [String, nil] group The user group to dedicate the node to. + # @return [void] + def dedicate_to_group(group) + body = [@name].compact + @conn.post("resources/node/groups/#{@group || "$ungrouped"}", body) do |r| + r.options.context = { + orka_auth_type: [:license, :token], + } + end + @orka_group = group + end + + # Make this node available to all users. + # + # @macro auth_token_and_license + # + # @note This is a BETA feature. + # + # @return [void] + def remove_group_dedication + dedicate_to_group(nil) + end + + private + + def lazy_initialize + # We don't use /resources/node/status/{name} as it only provides partial data. + url = "resources/node/list" + url += "/all" if @admin + response = @conn.get(url) do |r| + auth_type = [:token] + auth_type << :license if @admin + r.options.context = { + orka_auth_type: auth_type, + } + end + node = response.body["nodes"].find { |hash| hash["name"] == @name } + + raise ResourceNotFoundError, "No node found matching \"#{@name}\"." if node.nil? + + deserialize(node) + super + end + + def deserialize(hash) + @name = hash["name"] + @host_name = hash["host_name"] + @address = hash["address"] + @host_ip = hash["hostIP"] + @available_cpu = hash["available_cpu"] + @allocatable_cpu = hash["allocatable_cpu"] + @available_memory = hash["available_memory"] + @total_cpu = hash["total_cpu"] + @total_memory = hash["total_memory"] + @state = hash["state"] + @orka_group = hash["orka_group"] + end + end + end +end diff --git a/lib/orka_api_client/models/password_requirements.rb b/lib/orka_api_client/models/password_requirements.rb new file mode 100644 index 0000000..9a57653 --- /dev/null +++ b/lib/orka_api_client/models/password_requirements.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # The requirements enforced for passwords when creating a user account. + class PasswordRequirements + # @return [Integer] The minimum length of a password. + attr_reader :length + + # @api private + # @param [Hash] hash + def initialize(hash) + @length = hash["password_length"] + end + end + end +end diff --git a/lib/orka_api_client/models/protocol_port_mapping.rb b/lib/orka_api_client/models/protocol_port_mapping.rb new file mode 100644 index 0000000..9e04ef1 --- /dev/null +++ b/lib/orka_api_client/models/protocol_port_mapping.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "../port_mapping" + +module OrkaAPI + module Models + # Represents a port forwarding from a host node to a guest VM, with an additional field denoting the transport + # protocol. + class ProtocolPortMapping < PortMapping + # @return [String] The transport protocol, typically TCP. + attr_reader :protocol + + # @api private + # @param [Integer] host_port + # @param [Integer] guest_port + # @param [String] protocol + def initialize(host_port:, guest_port:, protocol:) + super(host_port: host_port, guest_port: guest_port) + @protocol = protocol + end + end + end +end diff --git a/lib/orka_api_client/models/remote_image.rb b/lib/orka_api_client/models/remote_image.rb new file mode 100644 index 0000000..cb0f5c2 --- /dev/null +++ b/lib/orka_api_client/models/remote_image.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # Represents an image which exists in the Orka remote repo rather than local storage. + class RemoteImage + # @return [String] The name of this remote image. + attr_reader :name + + # @api private + # @param [String] name + # @param [Connection] conn + def initialize(name, conn:) + @name = name + @conn = conn + end + + # Pull this image from the remote repo. This is a long-running operation and might take a while. + # + # The operation copies the image to the local storage of your Orka environment. The base image will be + # available for use by all users of the environment. + # + # @macro auth_token + # + # @param [String] new_name The name for the local copy of this image. + # @return [Image] The lazily-loaded local image. + def pull(new_name) + body = { + image: @name, + new_name: new_name, + }.compact + @conn.post("resources/image/pull", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + Image.lazy_prepare(name: new_name, conn: @conn) + end + end + end +end diff --git a/lib/orka_api_client/models/remote_iso.rb b/lib/orka_api_client/models/remote_iso.rb new file mode 100644 index 0000000..bdc1a24 --- /dev/null +++ b/lib/orka_api_client/models/remote_iso.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module OrkaAPI + module Models + # Represents an ISO which exists in the Orka remote repo rather than local storage. + class RemoteISO + # @return [String] The name of this remote ISO. + attr_reader :name + + # @api private + # @param [String] name + # @param [Connection] conn + def initialize(name, conn:) + @name = name + @conn = conn + end + + # Pull an ISO from the remote repo. You can retain the ISO name or change it during the operation. This is a + # long-running operation and might take a while. + # + # The operation copies the ISO to the local storage of your Orka environment. The ISO will be available for use + # by all users of the environment. + # + # @macro auth_token + # + # @param [String] new_name The name for the local copy of this ISO. + # @return [ISO] The lazily-loaded local ISO. + def pull(new_name) + body = { + image: @name, + new_name: new_name, + }.compact + @conn.post("resources/iso/pull", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + ISO.lazy_prepare(name: new_name, conn: @conn) + end + end + end +end diff --git a/lib/orka_api_client/models/token_info.rb b/lib/orka_api_client/models/token_info.rb new file mode 100644 index 0000000..72e87b1 --- /dev/null +++ b/lib/orka_api_client/models/token_info.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "attr_predicate" +require_relative "user" + +module OrkaAPI + module Models + # Provides information on the client's token. + class TokenInfo + extend AttrPredicate + + # @return [Boolean] True if the tokeb is valid for authentication. + attr_predicate :authenticated + + # @return [Boolean] True if the token has been revoked. + attr_predicate :token_revoked + + # @return [User] The user associated with the token. + attr_reader :user + + # @api private + # @param [Hash] hash + # @param [Connection] conn + def initialize(hash, conn:) + @authenticated = hash["authenticated"] + @token_revoked = hash["is_token_revoked"] + @user = Models::User.lazy_prepare(email: hash["email"], conn: conn) + end + end + end +end diff --git a/lib/orka_api_client/models/user.rb b/lib/orka_api_client/models/user.rb new file mode 100644 index 0000000..902b43d --- /dev/null +++ b/lib/orka_api_client/models/user.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require_relative "lazy_model" + +module OrkaAPI + module Models + # To work with Orka, you need to have a user with an assigned license. You will use this user and the respective + # credentials to authenticate against the Orka service. After being authenticated against the service, you can + # run Orka API calls. + class User < LazyModel + # @return [String] The email address for the user. + attr_reader :email + + # @return [String, nil] The group the user is in, if any. + lazy_attr :group + + # @api private + # @param [String] email + # @param [Connection] conn + # @return [User] + def self.lazy_prepare(email:, conn:) + new(conn: conn, email: email) + end + + # @api private + # @param [Connection] conn + # @param [String] email + # @param [String] group + def initialize(conn:, email:, group: nil) + super(!group.nil?) + @conn = conn + @email = email + @group = if group == "$ungrouped" + nil + else + group + end + end + public_class_method :new + + # Delete the user in the endpoint. The user must have no Orka resources associated with them (other than their + # authentication tokens). This operation invalidates all tokens associated with the user. + # + # @macro auth_token_and_license + # + # @return [void] + def delete + @conn.delete("users/#{@email}") do |r| + r.options.context = { + orka_auth_type: [:license, :token], + } + end + end + + # Reset the password for the user. This operation is intended for administrators. + # + # @macro auth_token_and_license + # + # @param [String] password The new password for the user. + # @return [void] + def reset_password(password) + body = { + email: email, + password: password, + }.compact + @conn.post("users/password", body) do |r| + r.options.context = { + orka_auth_type: [:license, :token], + } + end + end + + # Apply a group label to the user. + # + # @note This is a BETA feature. + # + # @macro auth_license + # + # @param [String] group The new group for the user. + # @return [void] + def change_group(group) + @conn.post("users/groups/#{group || "$ungrouped"}", [@email]) do |r| + r.options.context = { + orka_auth_type: :license, + } + end + @group = group + end + + # Remove a group label from the user. + # + # @note This is a BETA feature. + # + # @macro auth_license + # + # @return [void] + def remove_group + change_group(nil) + end + + private + + def lazy_initialize + groups = @conn.get("users") do |r| + r.options.context = { + orka_auth_type: :license, + } + end.body["user_groups"] + group = groups.find { |_, group_users| group_users.include?(@email) }&.first + + raise ResourceNotFoundError, "No user found matching \"#{@email}\"." if group.nil? + + @group = if group == "$ungrouped" + nil + else + group + end + super + end + end + end +end diff --git a/lib/orka_api_client/models/vm_configuration.rb b/lib/orka_api_client/models/vm_configuration.rb new file mode 100644 index 0000000..9535d6e --- /dev/null +++ b/lib/orka_api_client/models/vm_configuration.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require_relative "lazy_model" + +module OrkaAPI + module Models + # A template configuration (a container template) consisting of a + # {https://orkadocs.macstadium.com/docs/orka-glossary#base-image base image}, a + # {https://orkadocs.macstadium.com/docs/orka-glossary#snapshot-image snapshot image}, and the number of CPU cores + # to be used. To become a VM that you can run in the cloud, a VM configuration needs to be deployed to a {Node + # node}. + # + # You can deploy multiple VMs from a single VM configuration. Once created, you can no longer modify a VM + # configuration. + # + # Deleting a VM does not delete the VM configuration it was deployed from. + class VMConfiguration < LazyModel + # @return [String] The name of this VM configuration. + attr_reader :name + + # @return [User] The owner of this VM configuration, i.e. the user which deployed it. + lazy_attr :owner + + # @return [Image] The base image which newly deployed VMs of this configuration will boot from. + lazy_attr :base_image + + # @return [Integer] The number of CPU cores to allocate to deployed VMs of this configuration. + lazy_attr :cpu_cores + + # @return [ISO] The ISO to attach to deployed VMs of this configuration. + lazy_attr :iso_image + + # @return [Image] The storage disk image to attach to deployed VMs of this configuration. + lazy_attr :attached_disk + + # @return [Boolean] True if the VNC console should be enabled for deployed VMs of this configuration. + lazy_attr :vnc_console? + + # @return [Boolean] True if IO boost should be enabled for deployed VMs of this configuration. + lazy_attr :io_boost? + + # @return [Boolean] True if deployed VMs of this configuration should use a prior saved state (created via + # {VMInstance#save_state}) rather than a clean base image. + lazy_attr :use_saved_state? + + # @return [Boolean] True if GPU passthrough should be enabled for deployed VMs of this configuration. + lazy_attr :gpu_passthrough? + + # @api private + # @param [String] name + # @param [Connection] conn + # @return [VMConfiguration] + def self.lazy_prepare(name:, conn:) + new(conn: conn, name: name) + end + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @return [VMConfiguration] + def self.from_hash(hash, conn:) + new(conn: conn, hash: hash) + end + + # @private + # @param [Connection] conn + # @param [String] name + # @param [Hash] hash + def initialize(conn:, name: nil, hash: nil) + super(!hash.nil?) + @conn = conn + @name = name + deserialize(hash) if hash + end + + # Deploy the VM configuration to a node. If you don't specify a node, Orka chooses a node based on the + # available resources. + # + # @macro auth_token + # + # @param [Node, String] node The node on which to deploy the VM. The node must have sufficient CPU and memory + # to accommodate the VM. + # @param [Integer] replicas The scale at which to deploy the VM configuration. If not specified, defaults to + # +1+ (non-scaled). + # @param [Array] reserved_ports One or more port mappings that enable additional ports on your VM. + # @param [Boolean] iso_install Set to +true+ if you want to use an ISO. + # @param [Models::ISO, String] iso_image An ISO to attach to the VM during deployment. If already set in the + # respective VM configuration and not set here, Orka applies the setting from the VM configuration. You can + # also use this field to override any ISO specified in the VM configuration. + # @param [Boolean] attach_disk Set to +true+ if you want to attach additional storage during deployment. + # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM during + # deployment. If already set in the respective VM configuration and not set here, Orka applies the setting + # from the VM configuration. You can also use this field to override any storage specified in the VM + # configuration. + # @param [Boolean] vnc_console Enables or disables VNC for the VM. If not set in the VM configuration or here, + # defaults to +true+. If already set in the respective VM configuration and not set here, Orka applies the + # setting from the VM configuration. You can also use this field to override the VNC setting specified in the + # VM configuration. + # @param [Hash{String => String}] vm_metadata Inject custom metadata to the VM. If not set, only the built-in + # metadata is injected into the VM. + # @param [String] system_serial Assign an owned macOS system serial number to the VM. If already set in the + # respective VM configuration and not set here, Orka applies the setting from the VM configuration. + # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. If not set in the VM + # configuration or here, defaults to +false+. If already set in the respective VM configuration and not set + # here, Orka applies the setting from the VM configuration. You can also use this field to override the GPU + # passthrough setting specified in the VM configuration. When enabled, +vnc_console+ is automatically + # disabled. GPU passthrough is an experimental feature. GPU passthrough must first be enabled in your + # cluster. + # @return [void] + def deploy(node: nil, replicas: nil, reserved_ports: nil, iso_install: nil, + iso_image: nil, attach_disk: nil, attached_disk: nil, vnc_console: nil, + vm_metadata: nil, system_serial: nil, gpu_passthrough: nil) + VMResource.lazy_prepare(@name, conn: @conn).deploy( + node: node, + replicas: replicas, + reserved_ports: reserved_ports, + iso_install: iso_install, + iso_image: iso_image, + attach_disk: attach_disk, + attached_disk: attached_disk, + vnc_console: vnc_console, + vm_metadata: vm_metadata, + system_serial: system_serial, + gpu_passthrough: gpu_passthrough, + ) + end + + # Remove the VM configuration and all VM deployments of it. + # + # If the VM configuration and its deployments belong to the user associated with the client's token then the + # client only needs to be configured with a token. Otherwise, if you are removing a VM resource associated with + # another user, you need to configure the client with both a token and a license key. + # + # @return [void] + def purge + VMResource.lazy_prepare(@name, conn: @conn).purge + end + + # Delete the VM configuration state. Now when you deploy the VM configuration it will use the base image to + # boot the VM. + # + # To delete a VM state, it must not be used by any deployed VM. + # + # @macro auth_token + # + # @return [void] + def delete_saved_state + @conn.delete("resources/vm/configs/#{@name}/delete-state") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + private + + def lazy_initialize + response = @conn.get("resources/vm/configs/#{@name}") do |r| + r.options.context = { + orka_auth_type: :token, + } + end + configs = response.body["configs"] + + raise ResourceNotFoundError, "No VM configuration found matching \"#{@name}\"." if configs.empty? + + deserialize(configs.first) + super + end + + def deserialize(hash) + @name = hash["orka_vm_name"] + @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) + @base_image = Image.lazy_prepare(name: hash["orka_base_image"], conn: @conn) + @cpu_cores = hash["orka_cpu_core"] + @iso_image = if hash["iso_image"] == "None" + nil + else + ISO.lazy_prepare(name: hash["iso_image"], conn: @conn) + end + @attached_disk = if hash["attached_disk"] == "None" + nil + else + Image.lazy_prepare(name: hash["attached_disk"], conn: @conn) + end + @vnc_console = hash["vnc_console"] + @io_boost = hash["io_boost"] + @use_saved_state = hash["use_saved_state"] + @gpu_passthrough = hash["gpu_passthrough"] + end + end + end +end diff --git a/lib/orka_api_client/models/vm_instance.rb b/lib/orka_api_client/models/vm_instance.rb new file mode 100644 index 0000000..d81690b --- /dev/null +++ b/lib/orka_api_client/models/vm_instance.rb @@ -0,0 +1,468 @@ +# frozen_string_literal: true + +require_relative "attr_predicate" +require_relative "protocol_port_mapping" +require_relative "disk" + +module OrkaAPI + module Models + # A virtual machine deployed on a {Node node} from an existing {VMConfiguration VM configuration} or cloned from + # an existing virtual machine. In the case of macOS VMs, this is a full macOS VM inside of a + # {https://www.docker.com/resources/what-container Docker container}. + class VMInstance + extend AttrPredicate + + # @return [String] The ID of the VM. + attr_reader :id + + # @return [String] The name of the VM. + attr_reader :name + + # @return [Node] The node the VM is deployed on. + attr_reader :node + + # @return [User] The owner of the VM, i.e. the user which deployed it. + attr_reader :owner + + # @return [String] The state of the node the VM is deployed on. + attr_reader :node_status + + # @return [String] The IP of the VM. + attr_reader :ip + + # @return [Integer] The port used to connect to the VM via VNC. + attr_reader :vnc_port + + # @return [Integer] The port used to connect to the VM via macOS Screen Sharing. + attr_reader :screen_sharing_port + + # @return [Integer] The port used to connect to the VM via SSH. + attr_reader :ssh_port + + # @return [Integer] The number of CPU cores allocated to the VM. + attr_reader :cpu + + # @return [Integer] The number of vCPUs allocated to the VM. + attr_reader :vcpu + + # @return [String] The amount of RAM allocated to the VM. + attr_reader :ram + + # @return [Image] The base image the VM was deployed from. + attr_reader :base_image + + # @return [String] + attr_reader :image + + # @return [String] + attr_reader :configuration_template + + # @return [String] The status of the VM, at the time this class was initialized. + attr_reader :status + + # @return [Boolean] True if IO boost is enabled for this VM. + attr_predicate :io_boost + + # @return [Boolean] True if this VM is using a prior saved state rather than a clean base image. + attr_predicate :use_saved_state + + # @return [Array] The port mappings established for this VM. + attr_reader :reserved_ports + + # @return [DateTime] The time when this VM was deployed. + attr_reader :creation_time + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @param [Boolean] admin + def initialize(hash, conn:, admin: false) + @conn = conn + @admin = admin + deserialize(hash) + end + + # @!macro [new] vm_instance_state_note + # @note Calling this will not change the state of this object, and thus not change the return values of + # attributes like {#status}. You must fetch a new object instance from the client to refresh this data. + + # Remove the VM instance. + # + # If the VM instance belongs to the user associated with the client's token then the client only needs to be + # configured with a token. Otherwise, if you are removing a VM instance associated with another user, you need + # to configure the client with both a token and a license key. + # + # @macro vm_instance_state_note + # + # @return [void] + def delete + @conn.delete("resources/vm/delete") do |r| + r.body = { + orka_vm_name: @id, + }.compact + + auth_type = [:token] + auth_type << :license if @admin + r.options.context = { + orka_auth_type: auth_type, + } + end + end + + # Power ON the VM. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @return [void] + def start + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/exec/start", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Power OFF the VM. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @return [void] + def stop + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/exec/stop", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Suspend the VM. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @return [void] + def suspend + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/exec/suspend", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Resume the VM. The VM must already be suspended. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @return [void] + def resume + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/exec/resume", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Revert the VM to the latest state of its base image. This operation restarts the VM. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @return [void] + def revert + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/exec/revert", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # List the disks attached to the VM. The VM must be non-scaled. + # + # @macro auth_token + # + # @macro lazy_enumerator + # + # @return [Enumerator] The enumerator of the disk list. + def disks + Enumerator.new do + drives = @conn.get("resources/vm/list-disks") do |r| + r.options.context = { + orka_auth_type: :token, + } + end.body["drives"] + drives.map do |hash| + Disk.new( + type: hash["type"], + device: hash["device"], + target: hash["target"], + source: hash["source"], + ) + end + end + end + + # Attach a disk to the VM. The VM must be non-scaled. + # + # You can attach any of the following disks: + # + # * Any disks created with {Client#generate_empty_image} + # * Any non-bootable images available in your Orka storage and listed by {Client#images} + # + # @macro auth_token + # + # @note Before you can use the attached disk, you need to restart the VM with a {#stop manual stop} of the VM, + # followed by a {#start manual start} VM. A software reboot from the OS will not trigger macOS to recognize + # the disk. + # + # @param [Image, String] image The disk to attach to the VM. + # @param [String] mount_point The mount point to attach the VM to. + # @return [void] + def attach_disk(image:, mount_point:) + body = { + orka_vm_name: @id, + image_name: image.is_a?(Image) ? image.name : image, + mount_point: mount_point, + }.compact + @conn.post("resources/vm/attach-disk", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Scale the VM to the specified number of replicas. + # + # When you are working with scaled VMs, the following limitations are in place: + # + # * Some operations are not available for scaled VMs (saving an image, committing changes to a base image, + # cloning a VM, migrating a VM). + # * If you specify a node during deployment, scaling is limited to the resources of that node and will not + # create new replicas on other nodes. + # * All replicas share the same ID. + # * When you scale down, you have no control over which replicas will be destroyed and which ones will be + # preserved. This might result in loss of data. + # * When you scale up, newly deployed replicas use the base image of the original VM config. Changes from any + # other running replicas are not applied to the new replicas. + # * Deleting a VM deletes all replicas. + # + # @macro auth_token + # + # @param [Integer] replicas The number of replicas to scale to. + # @return [void] + def scale(replicas) + body = { + orka_vm_name: @id, + replicas: replicas, + }.compact + @conn.patch("resources/vm/scale", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Un-scale a scaled VM. This removes any additional replicas of the VM and leaves only one running copy. + # + # When you scale down, you have no control over which replicas will be destroyed and which one will be + # preserved. This might result in loss of data. + # + # @macro auth_token + # + # @return [void] + def unscale + scale(1) + end + + # Migrate the VM from its {#node current node} to another destination node. The destination node should be + # different from the source node. + # + # This operation changes the IP of the migrated VM and might change its assigned ports. This operation removes + # the VM from the source node. + # + # @macro auth_token + # + # @macro vm_instance_state_note + # + # @param [Node, String] destination_node The node to migrate the VM to. + # @return [void] + def migrate(destination_node) + body = { + orka_vm_name: @id, + current_node_name: @node.name, + new_nodes: [destination_node.is_a?(Node) ? node.name : node], + }.compact + @conn.post("resources/vm/migrate", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Clone the VM from its {#node current node} to another destination node. The destination node should be + # different from the source node. + # + # This operation retains the VM on the source node. The clone receives a new ID and might receive a new IP and + # assigned ports, based on the destination node. + # + # @macro auth_token + # + # @param [Node, String] destination_node The node to migrate the VM to. + # @return [void] + def clone(destination_node) + body = { + orka_vm_name: @id, + current_node_name: @node.name, + new_nodes: [destination_node.is_a?(Node) ? node.name : node], + }.compact + @conn.post("resources/vm/clone", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Save the VM configuration state (disk and memory). + # + # If VM state is previously saved, it is overwritten. To overwrite the VM state, it must not be used by any + # deployed VM. + # + # @macro auth_token + # + # @note Saving VM state is restricted only to VMs that have GPU passthrough disabled. + # + # @return [void] + def save_state + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/vm/configs/save-state", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Apply the current state of the VM's image to the original base image in the Orka storage. Use this operation + # to modify an existing base image. All VM configs that reference this base image will be affected. + # + # The VM must be non-scaled. The base image to which you want to commit changes must be in use by only one VM. + # The base image to which you want to commit changes must not be in use by a VM configuration with saved VM + # state. + # + # @macro auth_token + # + # @return [void] + def commit_to_base_image + body = { + orka_vm_name: @id, + }.compact + @conn.post("resources/image/commit", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Save the current state of the VM's image to a new base image in the Orka storage. Use this operation to + # create a new base image. + # + # The VM must be non-scaled. The base image name that you specify must not be in use. + # + # @macro auth_token + # + # @param [String] image_name The name to give to the new base image. + # @return [Image] The lazily-loaded new base image. + def save_new_base_image(image_name) + body = { + orka_vm_name: @id, + new_name: image_name, + }.compact + @conn.post("resources/image/save", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + Image.lazy_prepare(image_name, conn: @conn) + end + + # Resize the current disk of the VM and save it as a new base image. This does not affect the original base + # image of the VM. + # + # @macro auth_token + # + # @param [String] username The username of the VM user. + # @param [String] password The password of the VM user. + # @param [String] image_name The new name for the resized image. + # @param [String] image_size The size of the new image (in k, M, G, or T), for example +"100G"+. + # @return [Image] The lazily-loaded new base image. + def resize_image(username:, password:, image_name:, image_size:) + body = { + orka_vm_name: @id, + vm_username: username, + vm_password: password, + new_image_size: image_size, + new_image_name: image_name, + }.compact + @conn.post("resources/image/resize", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + Image.lazy_prepare(image_name, conn: @conn) + end + + private + + def deserialize(hash) + @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) + @name = hash["virtual_machine_name"] + @id = hash["virtual_machine_id"] + @node = Node.lazy_prepare(name: hash["node_location"], conn: @conn, admin: @admin) + @node_status = hash["node_status"] + @ip = hash["virtual_machine_ip"] + @vnc_port = hash["vnc_port"].to_i + @screen_sharing_port = hash["screen_sharing_port"].to_i + @ssh_port = hash["ssh_port"].to_i + @cpu = hash["cpu"] + @vcpu = hash["vcpu"] + @ram = hash["RAM"] + @base_image = Image.lazy_prepare(name: hash["base_image"], conn: @conn) + @image = hash["image"] # TODO: rename this? provide an object? + @configuration_template = hash["configuration_template"] + @io_boost = hash["io_boost"] + @use_saved_state = hash["use_saved_state"] + @reserved_ports = hash["reserved_ports"].map do |mapping| + ProtocolPortMapping.new( + host_port: mapping["host_port"], + guest_port: mapping["guest_port"], + protocol: mapping["protocol"], + ) + end + @creation_time = DateTime.iso8601(hash["creation_timestamp"]) + end + end + end +end diff --git a/lib/orka_api_client/models/vm_resource.rb b/lib/orka_api_client/models/vm_resource.rb new file mode 100644 index 0000000..d9b00b9 --- /dev/null +++ b/lib/orka_api_client/models/vm_resource.rb @@ -0,0 +1,355 @@ +# frozen_string_literal: true + +require_relative "lazy_model" +require_relative "vm_instance" + +module OrkaAPI + module Models + # A general representation of {VMConfiguration VM configurations} and the {VMInstance VMs} deployed from those + # configurations. + class VMResource < LazyModel + # @return [String] The name of this VM resource. + attr_reader :name + + # @return [Boolean] True if there are associated deployed VM instances. + lazy_attr :deployed? + + # @return [Array] The list of deployed VM instances. + lazy_attr :instances + + # @return [User, nil] The owner of the associated VM configuration. This is +nil+ if {#deployed?} is +true+. + lazy_attr :owner + + # @return [Integer, nil] The number of CPU cores to use, specified by the associated VM configuration. This is + # +nil+ if {#deployed?} is +true+. + lazy_attr :cpu + + # @return [Integer, nil] The number of vCPUs to use, specified by the associated VM configuration. This is + # +nil+ if {#deployed?} is +true+. + lazy_attr :vcpu + + # @return [Image, nil] The base image to use, specified by the associated VM configuration. This is +nil+ if + # {#deployed?} is +true+. + lazy_attr :base_image + + # @return [String, nil] + lazy_attr :image + + # @return [Boolean, nil] True if IO boost is enabled, specified by the associated VM configuration. This is + # +nil+ if {#deployed?} is +true+. + lazy_attr :io_boost? + + # @return [Boolean, nil] True if the saved state should be used rather than cleanly from the base image, + # specified by the associated VM configuration. This is +nil+ if {#deployed?} is +true+. + lazy_attr :use_saved_state? + + # @return [Boolean, nil] True if GPU passthrough is enabled, specified by the associated VM configuration. This + # is +nil+ if {#deployed?} is +true+. + lazy_attr :gpu_passthrough? + + # @return [String, nil] + lazy_attr :configuration_template + + # @api private + # @param [String] name + # @param [Connection] conn + # @param [Boolean] admin + # @return [VMResource] + def self.lazy_prepare(name:, conn:, admin: false) + new(conn: conn, name: name, admin: admin) + end + + # @api private + # @param [Hash] hash + # @param [Connection] conn + # @param [Boolean] admin + # @return [VMResource] + def self.from_hash(hash, conn:, admin: false) + new(conn: conn, hash: hash, admin: admin) + end + + # @private + # @param [Connection] conn + # @param [String] name + # @param [Hash] hash + # @param [Boolean] admin + def initialize(conn:, name: nil, hash: nil, admin: false) + super(!hash.nil?) + @conn = conn + @name = name + @admin = admin + deserialize(hash) if hash + end + + # @!macro [new] vm_resource_state_note + # @note Calling this will not change the state of this object, and thus not change the return values of + # {#deployed?} and {#instances}, if the object already been loaded. You must fetch a new object instance or + # call {#refresh} to refresh this data. + + # Deploy an existing VM configuration to a node. If you don't specify a node, Orka chooses a node based on the + # available resources. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node The node on which to deploy the VM. The node must have sufficient CPU and memory + # to accommodate the VM. + # @param [Integer] replicas The scale at which to deploy the VM configuration. If not specified, defaults to + # +1+ (non-scaled). + # @param [Array] reserved_ports One or more port mappings that enable additional ports on your VM. + # @param [Boolean] iso_install Set to +true+ if you want to use an ISO. + # @param [Models::ISO, String] iso_image An ISO to attach to the VM during deployment. If already set in the + # respective VM configuration and not set here, Orka applies the setting from the VM configuration. You can + # also use this field to override any ISO specified in the VM configuration. + # @param [Boolean] attach_disk Set to +true+ if you want to attach additional storage during deployment. + # @param [Models::Image, String] attached_disk An additional storage disk to attach to the VM during + # deployment. If already set in the respective VM configuration and not set here, Orka applies the setting + # from the VM configuration. You can also use this field to override any storage specified in the VM + # configuration. + # @param [Boolean] vnc_console Enables or disables VNC for the VM. If not set in the VM configuration or here, + # defaults to +true+. If already set in the respective VM configuration and not set here, Orka applies the + # setting from the VM configuration. You can also use this field to override the VNC setting specified in the + # VM configuration. + # @param [Hash{String => String}] vm_metadata Inject custom metadata to the VM. If not set, only the built-in + # metadata is injected into the VM. + # @param [String] system_serial Assign an owned macOS system serial number to the VM. If already set in the + # respective VM configuration and not set here, Orka applies the setting from the VM configuration. + # @param [Boolean] gpu_passthrough Enables or disables GPU passthrough for the VM. If not set in the VM + # configuration or here, defaults to +false+. If already set in the respective VM configuration and not set + # here, Orka applies the setting from the VM configuration. You can also use this field to override the GPU + # passthrough setting specified in the VM configuration. When enabled, +vnc_console+ is automatically + # disabled. GPU passthrough is an experimental feature. GPU passthrough must first be enabled in your + # cluster. + # @return [void] + def deploy(node: nil, replicas: nil, reserved_ports: nil, iso_install: nil, + iso_image: nil, attach_disk: nil, attached_disk: nil, vnc_console: nil, + vm_metadata: nil, system_serial: nil, gpu_passthrough: nil) + vm_metadata = { items: vm_metadata.map { |k, v| { key: k, value: v } } } unless vm_metadata.nil? + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + replicas: replicas, + reserved_ports: reserved_ports&.map { |mapping| "#{mapping.host_port}:#{mapping.guest_port}" }, + iso_install: iso_install, + iso_image: iso_image.is_a?(ISO) ? iso_image.name : iso_image, + attach_disk: attach_disk, + attached_disk: attached_disk.is_a?(Image) ? attached_disk.name : attached_disk, + vnc_console: vnc_console, + vm_metadata: vm_metadata, + system_serial: system_serial, + gpu_passthrough: gpu_passthrough, + }.compact + @conn.post("resources/vm/deploy", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Removes all VM instances. + # + # If the VM instances belongs to the user associated with the client's token then the client only needs to be + # configured with a token. Otherwise, if you are removing VM instances associated with another user, you need + # to configure the client with both a token and a license key. + # + # @macro vm_resource_state_note + # + # @param [Node, String] node If specified, only remove VM deployments on that node. + # @return [void] + def delete_all_instances(node: nil) + @conn.delete("resources/vm/delete") do |r| + r.body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + + auth_type = [:token] + auth_type << :license if @admin + r.options.context = { + orka_auth_type: auth_type, + } + end + end + + # Remove all VM instances and the VM configuration. + # + # If the VM resource belongs to the user associated with the client's token then the client only needs to be + # configured with a token. Otherwise, if you are removing a VM resource associated with another user, you need + # to configure the client with both a token and a license key. + # + # @macro vm_resource_state_note + # + # @return [void] + def purge + @conn.delete("resources/vm/purge") do |r| + r.body = { + orka_vm_name: @name, + }.compact + + auth_type = [:token] + auth_type << :license if @admin + r.options.context = { + orka_auth_type: auth_type, + } + end + end + + # Power ON all VM instances on a particular node that are associated with this VM resource. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node All deployments of this VM located on this node will be started. + # @return [void] + def start_all_on_node(node) + raise ArgumentError, "Node cannot be nil." if node.nil? + + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + @conn.post("resources/vm/exec/start", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Power OFF all VM instances on a particular node that are associated with this VM resource. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node All deployments of this VM located on this node will be stopped. + # @return [void] + def stop_all_on_node(node) + raise ArgumentError, "Node cannot be nil." if node.nil? + + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + @conn.post("resources/vm/exec/stop", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Suspend all VM instances on a particular node that are associated with this VM resource. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node All deployments of this VM located on this node will be suspended. + # @return [void] + def suspend_all_on_node(node) + raise ArgumentError, "Node cannot be nil." if node.nil? + + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + @conn.post("resources/vm/exec/suspend", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Resume all VM instances on a particular node that are associated with this VM resource. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node All deployments of this VM located on this node will be resumed. + # @return [void] + def resume_all_on_node(node) + raise ArgumentError, "Node cannot be nil." if node.nil? + + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + @conn.post("resources/vm/exec/resume", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + # Revert all VM instances on a particular node that are associated with this VM resource to the latest state of + # its base image. This operation restarts the VMs. + # + # @macro auth_token + # + # @macro vm_resource_state_note + # + # @param [Node, String] node All deployments of this VM located on this node will be reverted. + # @return [void] + def revert_all_on_node(node) + raise ArgumentError, "Node cannot be nil." if node.nil? + + body = { + orka_vm_name: @name, + orka_node_name: node.is_a?(Node) ? node.name : node, + }.compact + @conn.post("resources/vm/exec/revert", body) do |r| + r.options.context = { + orka_auth_type: :token, + } + end + end + + private + + def lazy_initialize + response = @conn.get("resources/vm/status/#{@name}") do |r| + auth_type = [:token] + auth_type << :license if @admin + r.options.context = { + orka_auth_type: auth_type, + } + end + resources = response.body["virtual_machine_resources"] + + raise ResourceNotFoundError, "No VM resource found matching \"#{@name}\"." if resources.empty? + + deserialize(resources.first) + super + end + + def deserialize(hash) + @name = hash["virtual_machine_name"] + @deployed = case hash["vm_deployment_status"] + when "Deployed" + true + when "Not Deployed" + false + else + raise UnrecognisedStateError, "Unrecognised VM deployment status." + end + + if @deployed + @instances = hash["status"].map { |instance| VMInstance.new(instance, conn: @conn, admin: @admin) } + else + @instances = [] + @owner = User.lazy_prepare(email: hash["owner"], conn: @conn) + @cpu = hash["cpu"] + @vcpu = hash["vcpu"] + @base_image = Image.lazy_prepare(name: hash["base_image"], conn: @conn) + @image = hash["image"] # TODO: rename this? provide an object? + @io_boost = hash["io_boost"] + @use_saved_state = hash["use_saved_state"] + @gpu_passthrough = hash["gpu_passthrough"] + @configuration_template = hash["configuration_template"] + end + end + end + end +end diff --git a/lib/orka_api_client/port_mapping.rb b/lib/orka_api_client/port_mapping.rb new file mode 100644 index 0000000..64987dc --- /dev/null +++ b/lib/orka_api_client/port_mapping.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module OrkaAPI + # Represents a port forwarding from a host node to a guest VM. + class PortMapping + # @return [Integer] The port on the node side. + attr_reader :host_port + + # @return [Integer] The port on the VM side. + attr_reader :guest_port + + # @param [Integer] host_port The port on the node side. + # @param [Integer] guest_port The port on the VM side. + def initialize(host_port:, guest_port:) + @host_port = host_port + @guest_port = guest_port + end + end +end diff --git a/lib/orka_api_client/version.rb b/lib/orka_api_client/version.rb new file mode 100644 index 0000000..01df993 --- /dev/null +++ b/lib/orka_api_client/version.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module OrkaAPI + class Client + # The version of this gem. + VERSION = "0.1.0" + + # The Orka API version this gem supports. Support for other versions is not guaranteed, in particular older + # versions. + API_VERSION = "1.7.0" + end +end diff --git a/orka_api_client.gemspec b/orka_api_client.gemspec new file mode 100644 index 0000000..68fe39b --- /dev/null +++ b/orka_api_client.gemspec @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "lib/orka_api_client/version" + +Gem::Specification.new do |spec| + spec.name = "orka_api_client" + spec.version = OrkaAPI::Client::VERSION + spec.authors = ["Bo Anderson"] + spec.email = ["mail@boanderson.me"] + + spec.summary = "Ruby library for interfacing with the MacStadium Orka API." + spec.homepage = "https://github.com/Homebrew/orka_api_client" + spec.required_ruby_version = ">= 2.6.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/Homebrew/orka_api_client" + spec.metadata["bug_tracker_uri"] = "https://github.com/Homebrew/orka_api_client/issues" + spec.metadata["changelog_uri"] = "https://github.com/Homebrew/orka_api_client/releases/tag/#{spec.version}" + spec.metadata["rubygems_mfa_required"] = "true" + + spec.files = Dir.chdir(File.expand_path(__dir__)) do + (Dir.glob("*.{md,txt}") + Dir.glob("{exe,lib,rbi}/**/*")).reject { |f| File.directory?(f) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "faraday", "~> 2.0" + spec.add_dependency "faraday-multipart", "~> 1.0" +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb new file mode 100644 index 0000000..2702f40 --- /dev/null +++ b/spec/client_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe OrkaAPI::Client do + # TODO +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2a00a22 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "orka_api_client" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end