# frozen_string_literal: true class Runner::Strategy::DockerContainerPool < Runner::Strategy attr_reader :container_id, :command, :execution_environment def self.config # Since the docker configuration file contains code that must be executed, we use ERB templating. @config ||= CodeOcean::Config.new(:docker).read(erb: true) end def self.request_from_management(environment) container_id = JSON.parse(Faraday.get("#{config[:pool][:location]}/docker_container_pool/get_container/#{environment.id}").body)['id'] container_id.presence || raise(Runner::Error::NotAvailable.new("DockerContainerPool didn't return a container id")) rescue Faraday::Error => e raise Runner::Error::Unknown.new("Faraday request to DockerContainerPool failed: #{e.inspect}") rescue JSON::ParserError => e raise Runner::Error::Unknown.new("DockerContainerPool returned invalid JSON: #{e.inspect}") end def initialize(runner_id, _environment) super @container_id = runner_id end def copy_files(files) FileUtils.mkdir_p(local_workspace_path) clean_workspace files.each do |file| if file.path.present? local_directory_path = local_path(file.path) FileUtils.mkdir_p(local_directory_path) end local_file_path = local_path(file.filepath) if file.file_type.binary? FileUtils.cp(file.native_file.path, local_file_path) else begin File.open(local_file_path, 'w') {|f| f.write(file.content) } rescue IOError => e # TODO: try catch i/o exception and log failed attempts # Does this fix the issue @Sebastian? What exceptions did you have in mind? raise Runner::Error::Unknown.new("Could not create workspace file #{file.filepath}: #{e.inspect}") end end end FileUtils.chmod_R('+rwX', local_workspace_path) end def destroy_at_management Faraday.get("#{self.class.config[:pool][:location]}/docker_container_pool/destroy_container/#{container.id}") rescue Faraday::Error => e raise Runner::Error::Unknown.new("Faraday request to DockerContainerPool failed: #{e.inspect}") end def attach_to_execution(command) @command = command starting_time = Time.zone.now query_params = 'logs=0&stream=1&stderr=1&stdout=1&stdin=1' websocket_url = "#{self.class.config[:ws_host]}/v1.27/containers/#{@container_id}/attach/ws?#{query_params}" EventMachine.run do socket = Connection.new(websocket_url, self) EventMachine.add_timer(@execution_environment.permitted_execution_time) do socket.status = :timeout destroy_at_management end socket.send(command) yield(socket) end Time.zone.now - starting_time # execution duration in seconds end private def container return @container if @container.present? @container = Docker::Container.get(@container_id) raise Runner::Error::RunnerNotFound unless @container.info['State']['Running'] @container rescue Docker::Error::NotFoundError raise Runner::Error::RunnerNotFound end def local_path(path) unclean_path = local_workspace_path.join(path) clean_path = File.expand_path(unclean_path) unless clean_path.to_s.start_with? local_workspace_path.to_s raise Runner::Error::Unknown.new("Local filepath #{clean_path.inspect} not allowed") end Pathname.new(clean_path) end def clean_workspace FileUtils.rm_r(local_workspace_path.children, secure: true) rescue Errno::ENOENT => e raise Runner::Error::Unknown.new("The workspace directory does not exist and cannot be deleted: #{e.inspect}") rescue Errno::EACCES => e # TODO: Why was this rescued before @Sebastian? raise Runner::Error::Unknown.new("Not allowed to clean workspace #{local_workspace_path}: #{e.inspect}") end def local_workspace_path @local_workspace_path ||= Pathname.new(container.binds.first.split(':').first) end class Connection < Runner::Connection def initialize(*args) @stream = 'stdout' super end def encode(data) "#{data}\n" end def decode(raw_event) case raw_event.data when /@#{@strategy.container_id[0..11]}/ # Assume correct termination for now and return exit code 0 # TODO: Can we use the actual exit code here? @exit_code = 0 @status = :terminated @socket.close when /#{format(@strategy.execution_environment.test_command, class_name: '.*', filename: '.*', module_name: '.*')}/ # TODO: Super dirty hack to redirect test output to stderr (remove attr_reader afterwards) @stream = 'stderr' when /#{@strategy.command}/ when /bash: cmd:canvasevent: command not found/ else {'type' => @stream, 'data' => raw_event.data} end end end end