|
| 1 | +require "ood_core/refinements/hash_extensions" |
| 2 | +require "json" |
| 3 | + |
| 4 | +# Utility class for the Coder adapter to interact with the Coders API. |
| 5 | +class OodCore::Job::Adapters::Coder::Batch |
| 6 | + require_relative "coder_job_info" |
| 7 | + class Error < StandardError; end |
| 8 | + def initialize(config) |
| 9 | + @host = config[:host] |
| 10 | + @token = config[:token] |
| 11 | + end |
| 12 | + |
| 13 | + def get_os_app_credentials(username, project_id) |
| 14 | + credentials_file = File.read("/home/#{username}/application_credentials.json") |
| 15 | + credentials = JSON.parse(credentials_file) |
| 16 | + credentials.find { |cred| cred["project_id"] == project_id } |
| 17 | + end |
| 18 | + |
| 19 | + def get_rich_parameters(coder_parameters, project_id, os_app_credentials) |
| 20 | + rich_parameter_values = [ |
| 21 | + { name: "application_credential_name", value: os_app_credentials["name"] }, |
| 22 | + { name: "application_credential_id", value: os_app_credentials["id"] }, |
| 23 | + { name: "application_credential_secret", value: os_app_credentials["secret"] }, |
| 24 | + {name: "project_id", value: project_id } |
| 25 | + ] |
| 26 | + if coder_parameters |
| 27 | + coder_parameters.each do |key, value| |
| 28 | + rich_parameter_values << { name: key, value: value.to_s} |
| 29 | + end |
| 30 | + end |
| 31 | + rich_parameter_values |
| 32 | + end |
| 33 | + |
| 34 | + def get_headers(coder_token) |
| 35 | + { |
| 36 | + 'Content-Type' => 'application/json', |
| 37 | + 'Accept' => 'application/json', |
| 38 | + 'Coder-Session-Token' => coder_token |
| 39 | + } |
| 40 | + end |
| 41 | + |
| 42 | + def submit(script) |
| 43 | + org_id = script.native[:org_id] |
| 44 | + project_id = script.native[:project_id] |
| 45 | + coder_parameters = script.native[:coder_parameters] |
| 46 | + endpoint = "https://#{@host}/api/v2/organizations/#{org_id}/members/#{username}/workspaces" |
| 47 | + os_app_credentials = get_os_app_credentials(username, project_id) |
| 48 | + headers = get_headers(@token) |
| 49 | + body = { |
| 50 | + template_id: script.native[:template_id], |
| 51 | + template_version_name: script.native[:template_version_name], |
| 52 | + name: "#{username}-#{script.native[:workspace_name]}-#{rand(2_821_109_907_456).to_s(36)}", |
| 53 | + rich_parameter_values: get_rich_parameters(coder_parameters, project_id, os_app_credentials), |
| 54 | + } |
| 55 | + |
| 56 | + resp = api_call('post', endpoint, headers, body) |
| 57 | + resp["id"] |
| 58 | + end |
| 59 | + |
| 60 | + def delete(id) |
| 61 | + endpoint = "https://#{@host}/api/v2/workspaces/#{id}/builds" |
| 62 | + headers = get_headers(@token) |
| 63 | + body = { |
| 64 | + 'orphan' => false, |
| 65 | + 'transition' => 'delete' |
| 66 | + } |
| 67 | + res = api_call('post', endpoint, headers, body) |
| 68 | + end |
| 69 | + |
| 70 | + def info(id) |
| 71 | + endpoint = "https://#{@host}/api/v2/workspaces/#{id}?include_deleted=true" |
| 72 | + headers = get_headers(@token) |
| 73 | + workspace_info_from_json(api_call('get', endpoint, headers)) |
| 74 | + end |
| 75 | + |
| 76 | + def coder_state_to_ood_status(coder_state) |
| 77 | + case coder_state |
| 78 | + when "starting" |
| 79 | + "queued" |
| 80 | + when "failed" |
| 81 | + "suspended" |
| 82 | + when "running" |
| 83 | + "running" |
| 84 | + when "deleted" |
| 85 | + "completed" |
| 86 | + when "stopped" |
| 87 | + "completed" |
| 88 | + else |
| 89 | + "undetermined" |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + def build_coder_job_info(json_data, status) |
| 94 | + coder_output_metadata = json_data["latest_build"]["resources"] |
| 95 | + &.find { |resource| resource["name"] == "coder_output" } |
| 96 | + &.dig("metadata") |
| 97 | + coder_output_hash = coder_output_metadata&.map { |meta| [meta["key"].to_sym, meta["value"]] }&.to_h || {} |
| 98 | + OodCore::Job::Adapters::Coder::CoderJobInfo.new(**{ |
| 99 | + id: json_data["id"], |
| 100 | + job_name: json_data["workspace_name"], |
| 101 | + status: OodCore::Job::Status.new(state: status), |
| 102 | + job_owner: json_data["workspace_owner_name"], |
| 103 | + submission_time: json_data["created_at"], |
| 104 | + dispatch_time: json_data.dig("updated_at"), |
| 105 | + wallclock_time: wallclock_time(json_data, status), |
| 106 | + ood_connection_info: { host: coder_output_hash[:floating_ip], port: 80 }, |
| 107 | + native: coder_output_hash |
| 108 | + }) |
| 109 | + end |
| 110 | + |
| 111 | + def wallclock_time(json_data, status) |
| 112 | + start_time = start_time(json_data) |
| 113 | + end_time = end_time(json_data, status) |
| 114 | + end_time - start_time |
| 115 | + end |
| 116 | + |
| 117 | + def start_time(json_data) |
| 118 | + start_time_string = json_data.dig("updated_at") |
| 119 | + DateTime.parse(start_time_string).to_time.to_i |
| 120 | + end |
| 121 | + |
| 122 | + def end_time(json_data, status) |
| 123 | + if status == 'deleted' |
| 124 | + end_time_string = json_data["latest_build"].dig("updated_at") |
| 125 | + et = DateTime.parse(end_time_string).to_time.to_i |
| 126 | + else |
| 127 | + et = DateTime.now.to_time.to_i |
| 128 | + end |
| 129 | + et |
| 130 | + end |
| 131 | + |
| 132 | + def workspace_info_from_json(json_data) |
| 133 | + state = json_data.dig("latest_build", "status") || json_data.dig("latest_build", "job", "status") |
| 134 | + status = coder_state_to_ood_status(state) |
| 135 | + build_coder_job_info(json_data, status) |
| 136 | + end |
| 137 | + |
| 138 | + def api_call(method, endpoint, headers, body = nil) |
| 139 | + uri = URI(endpoint) |
| 140 | + |
| 141 | + case method.downcase |
| 142 | + when 'get' |
| 143 | + request = Net::HTTP::Get.new(uri, headers) |
| 144 | + when 'post' |
| 145 | + request = Net::HTTP::Post.new(uri, headers) |
| 146 | + when 'delete' |
| 147 | + request = Net::HTTP::Delete.new(uri, headers) |
| 148 | + else |
| 149 | + raise ArgumentError, "Invalid HTTP method: #{method}" |
| 150 | + end |
| 151 | + |
| 152 | + request.body = body.to_json if body |
| 153 | + |
| 154 | + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| |
| 155 | + http.request(request) |
| 156 | + end |
| 157 | + |
| 158 | + case response |
| 159 | + when Net::HTTPSuccess |
| 160 | + JSON.parse(response.body) |
| 161 | + else |
| 162 | + raise Error, "HTTP Error: #{response.code} #{response.message} for request #{endpoint} and body #{body}" |
| 163 | + end |
| 164 | + end |
| 165 | + |
| 166 | + def username |
| 167 | + @username ||= Etc.getlogin |
| 168 | + end |
| 169 | + |
| 170 | +end |
0 commit comments